Skip to content

WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s - #1010

Draft
MauroToscano wants to merge 1168 commits into
mainfrom
whir-recursion-rpx
Draft

MauroToscano wants to merge 1168 commits into
mainfrom
whir-recursion-rpx

Conversation

@MauroToscano

@MauroToscano MauroToscano commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Draft. The WHIR pipeline's best configuration, complete on top of main. It contains:

  • the per-table GPU recursion;
  • the WHIR recursion, with its three optimisation rounds;
  • the ZisK-style proof-format levers;
  • the column-major LDE engine;
  • a batch of fixes to the gap against ZisK: a 27-variable WHIR stack, two WHIR memory kernels, leaner recursion
    programs, less idle time around the base, three faster RPX kernel paths and row-wise DEEP/OOD inversion;
  • grinding only before the queries in the WHIR chains (P2-W);
  • the argue's short, wide tables valued on the GPU (A1);
  • the argue's challenge tables built on the GPU (A2+A3);
  • pure WHIR recursion: every recursion proof is a WHIR proof, and level 1 verifies the base epochs directly;
  • the argue's big zerocheck batches run their programs on demand (N1′), and the RPX MDS compiles the same way in
    every build;
  • the card permit taken after the host prep;
  • the pure-WHIR tree at fan-in 5, and blocks of two to five epochs, which used to stop at the root;
  • the WHIR base's DECODE root computed beside epoch 0 (head ahead), and the GKR tree's refused reservations counted
    as device fallbacks;
  • the WHIR leaf layers kept through a budget miss they cannot cover, and whole WHIR trees kept on the card, both
    evictable, with a kept tree's promise given back by its last handle;
  • the argue's zerocheck computed the stage-1 way: the bus as one column, the weights pulled out, rounds 0 and 1 in
    the base field, integer interpolation nodes — every message the one it was;
  • the argue's GKR layers computed with Gruen's split (M1-1): two sums a round in registers, the previous fold
    riding the same pass, no layer-sized eq table, the host tail from a cube of 64 — every message the one it was;
  • the argue's GKR input layer written from the base columns in one launch a table, and no lift: the zerocheck's
    first pass reads the columns where they lie, so a table's factors are lifted only when the old rounds need them
    (M1-2) — every message the one it was;
  • level 1's card ordered: the WHIR global child proves after the last wide node has built its artifacts (a card
    latch, scheduling only);
  • WHIR grinding at 18 bits with 114 queries, in place of 20 bits and 112 (the minimum proven bits are unchanged at
    130.393);
  • main, merged.

Block 25368371 proves in 32.62 s, measured before the level-1 latch (FAST job 335). The latch's own A/B, on the
head before M1-2, read −0.43 s; it is a different A/B and is not added to that number.

What made it fast, ranked

These are the optimizations that took block 25368371 from 104.2 minutes to 32.62 s, ranked by the speedup each
measured when it landed. Each row is its own before/after at that time, so the rows do not add up.

# optimization landed measured speedup
1 proving on the GPU, one STARK per table, instead of the batched CPU pipeline 7–11 Sep 104.2 min → 21.8 min¹ 4.8×
2 the gap fixes: WHIR stack 27, the RPX limb permutation, the work-queue grind, base prep ahead of the prover thread, BITWISE only where used, Merkle tops per half-warp, the level-0 lead-in, and eight smaller 27–28 Sep WHIR 99.85 → 60.20 s · STARK 107.55 → 78.80 s 1.66× · 1.36×
3 a tree level's sibling proofs proved concurrently 14 Sep 418.5 → 252.7 s 1.66×
4 the proof-of-work grind on the GPU 11 Sep 21.8 → 13.6 min² 1.60×
5 FRI folds by 2^d per committed layer, one challenge each, with a verifier-side schedule (Haböck, eprint 2022/1216, Protocol 1): the recursion's FRI proofs lose about a third of their cells 24 Sep STARK 158.65 → 129.80 s · WHIR 128.20 → 120.85 s 1.22× · 1.06×
6 three WHIR tuning rounds: the VRAM budget read from the driver, evictable leaf-layer retention, tree fan-in 3 18–21 Sep 150.8 → 127.9 s 1.18×
7 less idle card in the STARK base (F-SIDLE), and the wraps attesting their program host-side (R1b) 29 Sep STARK 76.90 → 66.65 s 1.15×
8 the column-major LDE engine 24 Sep WHIR 106.80 → 100.65 s · STARK 118.60 → 103.90 s 1.06× · 1.14×
9 pure WHIR recursion: every recursion proof a WHIR proof, level 1 verifying the epochs directly 29 Sep 51.40 → 45.80 s 1.12×
10 the argue on the GPU: challenge tables (A2+A3), short wide tables (A1), the big batches' rounds on demand (N1′) 28–29 Sep 59.65 → 54.45 s · −1.00 s · −1.15 s 1.10× · 1.02× · 1.03×
11 the RPX MDS compiled the same way in every build, and NICE v2 (STARK) 29 Sep STARK 79.30 → 73.35 s, then 73.75 → 72.20 s · WHIR −0.50 s 1.08× · 1.02× · 1.01×
12 a six-variable first WHIR fold, schedule [6,4,4,4,4,3]: one round and three grinds fewer per chain, so fewer base commits, rebuilds and grinds 24 Sep WHIR 126.85 → 117.55 s 1.08×
13 the argue's zerocheck the stage-1 way: the bus as one column, the weights pulled out (Gruen), rounds 0 and 1 in the base field, integer interpolation nodes — the same messages 30 Sep 40.23 → 37.48 s (at 9e2728955) · 37.80 → 35.95 s at f3d359998 1.07× · 1.05×
14 the argue's GKR layers with Gruen's split: two sums a round, the fold riding the next round's pass, no layer-sized eq table, the host tail from a cube of 64 — the same messages 30 Sep 36.42 → 34.85 s (GKR 4.24 → 2.13 s) 1.05×
15 the argue's GKR input layer from the base columns (one launch a table, base × extension, where two program launches an interaction ran over lifted factors), then no lift (the zerocheck's first pass reads the columns; the freed room ends the kept WHIR trees' evictions) — the same messages 1 Oct 34.35 → 33.62 s · 34.17 → 32.62 s 1.02× · 1.05×
16 one-row openings with a committed FRI input (STARK only; +3.20 s on WHIR, so off there), and Merkle caps (c ≤ 3) on the STARK and WHIR trees 24–25 Sep one-row: STARK 157.45 → 149.45 s · caps: WHIR −1.85 s (STARK trees) and −0.95 s (WHIR trees) 1.05× · 1.01× each
17 the last scheduling and shape levers: grinding only before the queries (P2-W), fan-in 5, the card permit after the host prep with fan-in 4, keeping the WHIR leaf layers through a futile miss, keeping whole WHIR trees, the base's head ahead, the level-1 latch 28 Sep–1 Oct −2.30 · −1.85 · −1.15 · −1.25 · −1.00 · −0.70 · −0.43 s 1.01–1.05× each

¹ The move also changed the hardware, from a CPU box to one RTX 5090 on a Ryzen 9950X host.
² On a Ryzen 9950X + RTX 5090 box. The base alone fell from 156.3 to 67.9 s.

  • The proof formats together (rows 5, 12 and 16, each against the legacy format on one binary): WHIR 128.00 →
    107.45 s, STARK 157.45 → 121.65 s. On STARK the caps add no wall on top of the 2^d folds, though they remove
    another 1.4 M permutations.
  • WHIR against STARK: the WHIR pipeline was 1.07× faster than the STARK one at its first version (150.8 against
    161.4 s, 18 Sep), and is 1.84× faster today (32.62 against 60.18 s). Rows 6, 9, 10, 12, 13, 14 and 15, and most
    of 17, are WHIR-only; row 7 and the one-row openings are STARK-only.
  • Where this PR stands: 6.1× ZisK (5.37 s) and 3.7× SP1 (8.92 s, one compressed proof) on the same card, from
    11.2× and 6.7× on 28 Sep.

The number

Block 25368371 on the FAST box (Ryzen 9 9950X, RTX 5090 32 GB), RPX commitments, 15 epochs at 2^21. The four
no-lift arms of the last A/B (FAST job 335, eight arms, A B B A A B B A) read 32.3, 32.7, 32.6 and 32.9 s (mean
32.62 s). They were measured at ba784bd0b with LAMBDA_VM_ARGUE_GKR_INPUT=1 LAMBDA_VM_ARGUE_NO_LIFT=1; d108cabb5
is that commit plus the flip that makes both the defaults, so it runs the same code by default, and was gated, not
re-timed. The landing head 88b0d3196 is d108cabb5 plus the level-1 latch, on by default. The latch was timed in its
own A/B on 7d82a320f (job 255, −0.43 s), not on top of M1-2, so 32.62 s is quoted without it and the two gains are not
added. Host peak 16.3–19.9 GiB (the level-1 permit race of the M1-1 section, in both settings), device peak
26.4–27.1 GiB (27,122 MiB at most; the arms without no lift 28.9–29.2 GiB).

Each step below is its own ABBA on one binary: two arms per setting, alternated.

step before after Δ
legacy format → default format (the format levers) 128.00 s (127.8, 128.2), 32.5 GiB, 10.27 M permutations 107.45 s (107.1, 107.8), 23.8 GiB, 6.49 M permutations −20.55 s (−16.1 %)
per-level LDE → column-major LDE engine 106.80 s (106.5, 107.1), 24.1 GiB 100.65 s (100.8, 100.5), 23.3 GiB −6.15 s (−5.8 %)
every gap fix's opt-out set → the defaults at d1dc45514 99.85 s (99.6, 100.1), 23.5 GiB 60.20 s (59.8, 60.6), 19.6 GiB −39.65 s (−39.7 %)
grind before all three challenges → before the queries only (P2-W) 60.10 s (60.3, 59.9), 19.9 GiB 57.80 s (58.2, 57.4), 19.9 GiB −2.30 s (−3.8 %)
the argue's short, wide tables valued on the host → on the card (A1) 57.55 s (57.4, 57.7), 20.0 GiB 56.55 s (56.3, 56.8), 19.7 GiB −1.00 s (−1.7 %)
the argue's challenge tables built on the host → on the card (A2+A3)¹ 59.65 s (59.5, 59.8), 19.6 GiB 54.45 s (54.4, 54.5), 19.6 GiB −5.20 s (−8.7 %)
the per-table STARK recursion → pure WHIR recursion 51.40 s (51.7, 51.1), 19.6 GiB 45.80 s (45.7, 45.9), 16.1 GiB −5.60 s (−10.9 %)
the RPX MDS as a closure → over a compile-time matrix² 45.65 s (45.7, 45.6) 45.15 s (45.2, 45.1) −0.50 s (−1.1 %)
the argue's big batches' rounds held → on demand (N1′), at 5f15641b9 45.00 s (45.0, 45.0), 16.3 GiB 43.85 s (43.8, 43.9), 16.1 GiB −1.15 s (−2.6 %)
the permit before the prep at fan-in 3 → after the prep at fan-in 4, at 4ab853c6c 43.70 s (43.7, 43.7), 16.2 GiB 42.55 s (42.4, 42.7), 18.6 GiB −1.15 s (−2.6 %)
the pure-WHIR tree at fan-in 4 → fan-in 5, at b682091a3 42.85 s (42.7, 43.0), 18.9 GiB 41.00 s (40.8, 41.2), 16.2 GiB −1.85 s (−4.3 %)
the base's DECODE root before epoch 0 → beside it (head ahead), at 33232d688 40.90 s (40.9, 40.9), 16.2 GiB 40.20 s (40.3, 40.1), 17.3 GiB −0.70 s (−1.7 %)
a futile budget miss evicts the WHIR leaf layers → keeps them (bb7f5d8ee, its knob; default at 73342bc66) 39.95 s (39.9, 40.0), 16.3 GiB 38.70 s (38.8, 38.6), 16.2 GiB −1.25 s (−3.1 %)
the leaf layers kept → whole trees kept, evictable (22b81fc63, its knob; default at 7c8272701) 38.80 s (38.8, 38.8), 16.2 GiB 37.80 s (37.6, 38.0), 16.2 GiB −1.00 s (−2.6 %)
both opted out → both on, at 7c8272701 40.30 s (39.9, 40.7), 16.1 GiB 38.00 s (37.6, 38.4), 16.3 GiB −2.30 s (−5.7 %)
today's zerocheck rounds → stage 1 with integer nodes (d52f9dff6 on 9e2728955; two A B B A, pooled) 40.23 s (40.2, 40.2, 40.2, 40.3), 16.3 GiB 37.48 s (37.4, 37.4, 38.0, 37.1), 16.4 GiB −2.75 s (−6.8 %)
both opted out → both on, at f3d359998 (eight arms) 37.80 s (37.7, 37.7, 37.8, 38.0), 16.2 GiB 35.95 s (36.2, 35.7, 35.5, 36.4), 16.4 GiB −1.85 s (−4.9 %)
today's GKR layer rounds → Gruen's (M1-1), at 1946609c3 + the knob (eight arms) 36.42 s (36.2, 36.6, 36.5, 36.4), 16.3 GiB 34.85 s (35.0, 35.3, 33.9, 35.2), 16.3–20.0 GiB −1.57 s (−4.3 %)
the GKR input layer from the lifted factors → from the base columns (M1-2a), at 87ed8dbcd + the knob (eight arms) 34.35 s (34.4, 33.8, 34.6, 34.6), 16.2–16.4 GiB 33.62 s (34.1, 33.0, 33.0, 34.4), 16.2–19.9 GiB −0.73 s (−2.1 %)
the factors lifted → no lift (M1-2b), at ba784bd0b + both knobs, over M1-2a (eight arms) 34.17 s (33.8, 34.5, 34.2, 34.2), 19.9–20.1 GiB 32.62 s (32.3, 32.7, 32.6, 32.9), 16.3–19.9 GiB −1.55 s (−4.5 %)
level 1's card first come, first served → the global child after the last wide node's artifacts (the latch), at 208e2ff06 + the knob, on 7d82a320f (eight arms) 34.58 s (35.0, 34.6, 33.7, 35.0), 16.2–19.9 GiB 34.15 s (34.4, 34.1, 33.8, 34.3), 16.3–19.8 GiB −0.43 s (−1.2 %)

¹ Measured on its stage branch (34c17603b), before P2-W and A1.
² Two builds, alternated X XF X XF, at 6a6e26611: the MDS fix has no knob.

The keep-futile and whole-trees rows are each the decision A/B on the head before it; the row after them is the pair's
at-head A/B (A = LFM_WHIR_KEEP_FUTILE=0 LFM_WHIR_WHOLE_TREES=0). The stage-1 rows are its decision A/B (job 274 and
its replication, job 270) and its at-head A/B (A = LAMBDA_VM_ARGUE_FUSED=0 LAMBDA_VM_ARGUE_INT_NODES=0); at that head
the whole-tree retention takes back 0.52 s of the stage's base gain (see its section). The M1-1 row is its decision
A/B (job 330) on f3d359998, the stage-1 head; 7d82a320f is that A/B's B setting as the default. The M1-2 rows
are its two decision A/Bs (jobs 333 and 335) on 7d82a320f; d108cabb5 is the second A/B's B setting as the
default. The latch row is its A/B (job 255) on 7d82a320f, beside the M1-2 A/Bs rather than on top of them, with four
arms a setting (t ≈ −1.3; see its section); 88b0d3196 is d108cabb5 plus the latch with that A/B's B setting as the
default.

In the third row's A arms, every fix in the next table is switched off by its opt-out. Their program ids and census equal
those before the batch (668a89a4c). The one change with no opt-out of its own, the WHIR encoding through the engine,
is in both arms. A fifth arm, the defaults with only the level-0 lead-in off, read 61.4 s, so the lead-in is worth
−1.20 s here. The STARK PR (#1009) measured the same batch at −28.75 s (107.55 → 78.80 s).

The gap fixes

Each fix was measured first in its own ABBA, mostly on the base before the engine. Those rows do not add up to the
cumulative −39.65 s; the last row above is the measurement.

fix what changes opt-out its own ABBA
WHIR stack 27 a stacked polynomial may have 27 variables instead of 25: 50 base chains (331 rounds) instead of 145 (866) LAMBDA_VM_ZF_WHIR_STACK=25 −13.80 s (engine base, kernels and room on)
RPX limb permutation (K5) every RPX kernel runs the permutation with 32-bit limb multiplies and squarings unrolled by four, instead of the 64-bit multiply LAMBDA_VM_RPX_LIMB_PERMUTE=0 −10.05 s
RPX work-queue grind (K4) the proof-of-work search claims nonces 32 at a time from a work queue on a card-filling grid, instead of striding over a fixed grid; it finds the same smallest nonce LAMBDA_VM_RPX_GRIND_QUEUE=0 −6.45 s
base prep ahead of the prover thread each epoch's host preparation, and the global proof's, runs on the producer thread LAMBDA_VM_BASE_PREP_ON_PROVER=1 −5.40 s
BITWISE only where used recursion programs whose chips send BITWISE no lookup drop the fixed 2^20-row table, 26.2 M cells a proof LAMBDA_VM_LFM_KEEP_BITWISE=1 −4.70 s
RPX Merkle tops per half-warp (K3) a Merkle level of up to 16,384 pairs hashes one parent per half-warp, and the last 64 pairs run in one block LAMBDA_VM_RPX_WARP_MERKLE=0 −3.90 s
level-0 lead-in (I7) two helpers build level 0's first wrap prologues in the base's tail LFM_TREE_PROLOGUES_AT_LEVEL0=1 −3.20 s; −1.20 s at d1dc45514
lean WHIR coset fold the wraps emit the WHIR fold as (a − c)·w + c: three rows a value instead of seven LAMBDA_VM_WHIR_FOLD_CLASSIC=1 −2.85 s
per-transfer pinned staging (I6) each row-major commit transfer is staged through a pinned pair of its own, instead of a shared slab whose mutex serialised uploads and downloads LAMBDA_VM_STAGING_SHARED_SLAB=1 −2.10 s
WHIR memory kernels a round's six fold levels in one launch; the first six opening rounds read the shares instead of a materialised stack LAMBDA_VM_NO_WHIR_FUSED_FOLD=1, LAMBDA_VM_WHIR_LEAN_ROUNDS=0 −2.05 s
level 0 reuses the base's DECODE level 0 takes the DECODE commitment and prepared opening the base already derived LFM_TREE_REDERIVE_DECODE=1 −1.50 s
WHIR encoding through the engine the base commit's encoding goes through the column-major engine LAMBDA_VM_LDE_LEGACY=1, which also reverts every other LDE −0.65 s, device −1.0 GiB at stack 25
row-wise DEEP/OOD inversion (K6) the DEEP and OOD denominators are inverted row-wise, the DEEP kernel inverts its own, and the single-point OOD sums are row-chunked LAMBDA_VM_DEEP_INV_LEGACY=1 −0.30 s, inside the noise (its kernels −50 %); on by default because it measured −0.70 s and −2.35 GiB of device peak on #1009's pipeline
the room, parked and turn-sized a group's VRAM room is given back during the argument and taken back sized to the turn it covers LAMBDA_VM_NO_WHIR_ROOM_PARK=1, LAMBDA_VM_NO_WHIR_ROOM_RESIZE=1 wall-neutral at stack 25; −1.8 GiB of device ledger, which is what lets stack 27 fit
LFM_HASH split a recursion program's hash table is split in two when that saves ≥ 2^15 padded rows off here; LAMBDA_VM_LFM_HASH_SPLIT=1 turns it on +2.80 s on this pipeline, so off (on in #1009)

Also in the batch, with no knob:

  • The NTT and Möbius tile grids split past CUDA's grid.y limit, which stack 27's commits need.
  • Device commit and tree errors are logged and counted.
  • Each recursion census panel is printed in one write.

Grinding only before the queries (P2-W)

What changed. Each round of a WHIR base chain used to grind 20 bits before three challenges: the first folding
challenge, the out-of-domain batching challenge γ and the query positions. It now grinds before the query positions
only.

  • That is one grind a round instead of three (two on the last round): 518 grinds over the block's 82 chains instead of
    1,472.
  • The query count stays 112, because it reads the query grind alone.
  • The switch is a seventh ZF lever, whir_grind, default query; the banner reads … whir_stack=27 whir_grind=query.

The proof carries only the nonces it spends (NonceLayout::Spent).

  • In the recursion's input, the wrap's arena, an unspent nonce has no word: one nonce word a round instead of three,
    1,036 fewer a block.
  • The host proof struct keeps its three nonce fields, so the old format keeps its bytes. The host verifier refuses a
    nonzero value in a field the format does not carry.

Measured on block 25368371 (FAST, one binary, arms A B B A, wt820–823):

wall base host peak level 0 + global census
A: LAMBDA_VM_ZF_WHIR_GRIND=all 60.10 s (60.3, 59.9) 41.65 s 19.9 GiB the previous program ids and census, exactly
B: the default 57.80 s (58.2, 57.4) 39.15 s 19.9 GiB −49,174 instructions, −2,862 hash permutations, cells unchanged
Δ −2.30 s (−3.8 %) −2.50 s 0.0 GiB the wraps verify 954 fewer grinds
  • The chain grind time, summed over the base's 16 proofs, fell from 3.45 s to 1.20 s. That is ×0.347, against ×0.352
    predicted from the grind count.
  • Level 0 and the interior stayed within noise (0.00 s, +0.15 s), and so did the device peak (−112 MiB).

Soundness: no proven bits are lost. Of a round's three grinds, only the query grind raises the proven minimum as
placed:

  • The folding grind sits before the round's first sumcheck message. A cheating prover redraws the first folding
    challenge by varying that message, without grinding again, so this grind earned no credit.
  • The out-of-domain grind comes after the out-of-domain point. The only challenge it guards is γ, which has 176.95 bits
    with no grind at all.
  • The query grind sits right before the positions. It stays, and so do the 112 queries.

Every phase keeps its bits:

  • The WHIR chain minimum is 130.393 bits at stack 27, set by the first fold, which was already unground.
  • The pipeline minimum stays 128.946 bits, set by the LFM query phase (BCHKS25 Thm 4.2, Johnson regime, the calculator
    security/zisk_calc.py).
  • The grind bits are verifier-side constants absorbed in the statement ([0,0,20] against [20,20,20]), so a proof
    ground one way does not verify the other.
  • Tests:
    • a wrong query nonce is refused in every round, on the host and in the wrap program;
    • a set unspent nonce is refused by the host and has no way into the wrap's arena.
    • Mutations that remove the host's refusal, or give the unspent nonces their arena words back, turn those tests
      red.

Opt-out. LAMBDA_VM_ZF_WHIR_GRIND=all restores the grinds before all three challenges and the three-nonce format:
the previous proofs, program ids and census, byte for byte. Golden tests on the chain programs, arenas and host proof
bytes pin it, and so does the A arms' match above.

The argue's short, wide tables on the GPU (A1)

What changed. In the WHIR base's argue, a table whose columns are already resident on the card is now valued there
once it holds 2^16 cells (width × rows). Before, each column needed 2^16 rows. The few columns left on the host are
walked across the thread pool.

  • 68 tables move to the card, the largest the 1,480-wide precompile table at 2^15 rows. The columns valued on the host
    fall from 24,195 to 3,478 a block.
  • A table that is not resident keeps the old rule, since it would pay an upload.

Measured on block 25368371 (FAST, one binary, arms A B B A):

A: LAMBDA_VM_ARGUE_DEVICE_COLUMNS=0 B: the default Δ
the stage's own ABBA, before P2 (93a2b5643, wt831–834) 59.75 s (59.8, 59.7) 58.70 s (58.7, 58.7) −1.05 s
at its landing head (8930490e5, wt850–853) 57.55 s (57.4, 57.7) 56.55 s (56.3, 56.8) −1.00 s

The whole saving is in the base (41.90 → 40.95 s in the stage's ABBA); level 0 and the interior stay within noise.

Soundness: the proof does not change. The card computes each column's multilinear value exactly, the same field
element the host computes, so the transcript, every challenge and the serialized proof are identical, and so are the
program ids and census.

  • A cross-check arm (LAMBDA_VM_ARGUE_XCHECK=1) re-evaluated every card value on the host: 30,086 of 30,086 matched.
  • Tests: card against host from 2^1 to 2^15 rows and up to 2,000 columns; the whole argument's bytes with the columns
    on the card; a wrong card value is refused.

Opt-out. LAMBDA_VM_ARGUE_DEVICE_COLUMNS=0 restores the height rule and the one-at-a-time host walk, line for line.

The argue's challenge tables on the GPU (A2+A3)

What changed. The WHIR base's argue built its challenge-dependent tables on the host and uploaded them: the
zerocheck's eq(r) and eq(row) weights, and the claim reduce's shift tables and batched columns. It now builds them
on the card from the columns already resident there.

  • Each shift table is two device-to-device copies of one eq(α) table; each batched column is one kernel over the
    resident columns.
  • On the head's trace this removes the host builders' pool waits on the argue thread (3.56 s) and about 20 GB of
    pageable uploads a block (1.16 s of copies).

Measured on its stage branch (34c17603b, before P2-W and A1; wt836–839): A 59.65 s (59.5, 59.8) → B 54.45 s
(54.4, 54.5), −5.20 s. The base fell 41.65 → 36.65 s and the argue 19.7 → 14.7 s; the card built 1,364 tables an
arm.

Soundness: the proof does not change. The card builds the same field elements the host built, so the transcript,
every challenge and the serialized proof are identical.

  • A cross-check arm (LAMBDA_VM_ARGUE_XCHECK=1) compared every card table with the host's before its first round.
  • A corrupted card table is refused by the claim reduce (the first check that reads it), and a mutation that makes
    the fault inert fails that test.

Opt-out. LAMBDA_VM_ARGUE_DEVICE_TABLES=0 builds the tables on the host and uploads them, as before.

Pure WHIR recursion

What changed. The recursion's LFM proofs (the global wrap, the nodes and the block-artifact root) are proved by the
base's own stacked-WHIR prover instead of one STARK per table. Each parent verifies a child with the WHIR verifier the
wraps already run, over the child's LFM statement.

  • Level 1 verifies its epochs directly. A level-1 node checks three base epochs in one program: the wrap's verifier
    three times, then the node's own bindings and publishes. The 15 wraps and their proofs are gone. With three epochs a
    node, the tree's fan-in, the node schema, the root's L2G fold and every level above are unchanged.
  • Each table's instruction columns are committed once per program in a prepared stack, and opened at the table's
    own point. The main stack holds the value columns only.
  • The level-0 lead-in builds the level-1 nodes' programs in the base's tail, as it built the wraps': three epochs'
    harvests and the emission.
  • The recursion shrinks: cells from 3,233.7 M to 1,638.6 M (−49 %), hash permutations from 4.51 M to 2.00 M. The
    global wrap's program is the same one.

Measured on block 25368371 (FAST, one binary per row, arms A B B A):

A: the per-table STARK recursion B: the default Δ
the decision ABBA, before P2-W and A1 (4150afab6, wt860–863) 60.70 s (60.4, 61.0) 56.65 s (56.7, 56.6) −4.05 s
at this head (6a6e26611, wt880–883; A = LAMBDA_VM_LFM_PROVER=stark) 51.40 s (51.7, 51.1) 45.80 s (45.7, 45.9) −5.60 s
  • Where the time goes (at this head):
    • the base is unchanged, 32.9 s in both arms;
    • the recursion falls from 18.1 / 17.5 s to 12.4 s: level 1 (five level-1 nodes and the global wrap) takes 9.6 s
      against the wraps' 7.4 s, the interior 1.8 s against 9.0 s, the root 1.0 s against 1.4 s.
  • Host peak: 19.5–19.7 GiB → 16.1 GiB. Failures 0; every root proved and verified at 180 words.
  • One pre-registered row missed at this head. The lead-in had 2 of its 5 programs ready at level 1's start, against
    ≥ 4. The base is 9 s faster than when that row was set, which leaves the two helpers a 4.7 s tail. The property the
    count stood for held: the GPU's first hold came 0.17 s after level 1 started.

Soundness: every phase keeps ≥ 128 proven bits, and the recursion's minimum rises.

  • The chains: each recursion proof's chains use the base's own format: rate 1/4, 112 queries, 20-bit query grind
    (114 queries and an 18-bit grind since the grind-18 change below), stacks of 2^24–2^27. Their minimum is 130.393 bits, the n = 27 first fold, unground, the same as the base's. It
    replaces the STARK recursion's 128.946 bits, which came from its LFM query phase. Measured by the calculator on each
    run's own chain lines.
  • The instruction columns bind the program. An LFM AIR declares its preprocessed columns by count only, so a
    statement built from the AIR's column list would count zero and bind nothing: a forged program would verify.
    • The WHIR recursion's statement takes each table's count from the AIR.
    • The verifier refuses a counted prefix that no prepared opening settles.
    • The prepared roots are program constants, folded into the program id.
  • Public words: the in-guest verifier reads each public word as four base felts and absorbs them into the
    statement. The sponge receives each as a base token, so a non-canonical upper lane is unprovable.
  • The level-1 node binds its epochs with the node's own code: one attestation id, each epoch's FINI against the
    next one's INIT, each epoch at its tree position. It reads these from what the wraps would have published. Its L2G
    item is the fold of the epochs' bookend roots, the fold a node over those wraps takes.
  • Tests:
    • The count trap, both ways: the forged program verifies under a count-zero statement and is refused under the
      AIR's count. A forged instruction column is refused by the prepared opening. A deleted opening, a restated table
      height, a tampered or reordered public word and a prepared root absorbed after z are each refused.
    • The main stack without the prefix: a forged prefix, a proof read under the other layout and a left-out prefix
      that nothing settles are each refused.
    • The in-guest verifier executes an honest child and refuses seven arena mutations.
    • The level-1 node: it publishes an L1 node's schema. A broken register chain, swapped positions and a
      disagreeing attestation id are each refused, each beside a control without the bindings that executes.
    • Cost forms: they equal the emitted verifier exactly, per operation kind.
  • Instead of deletion mutations, the count check is shown load-bearing by those paired tests: the same forged proof
    verifies with the count at zero and is refused with the AIR's count.

Opt-outs.

  • LAMBDA_VM_LFM_PROVER=stark restores the per-table STARK recursion. The A arms above print 8930490e5's 24 program
    ids byte for byte.
  • LAMBDA_VM_LFM_WHIR_PREP=both also keeps each table's instruction columns in the main stack.
  • LAMBDA_VM_LFM_WIDE=off keeps the wraps under the WHIR recursion.

Narrow sumcheck rounds on demand (N1′)

What changed. In the WHIR argue's zerocheck, a big batch's device rounds now walk its program on demand.

  • Each step is emitted where the step that uses it first needs it, in Sethi–Ullman order: of two operands, the one
    needing more values goes first.

  • Every read, a column's value or a constant, is emitted again at each use instead of once and held.

  • The steps are the same operations on the same operands. What changes is how many values a thread holds at once,
    which sizes the round kernel's per-thread slot file and so how many threads a round can run.

  • The slot file is also sized for every interpolation node from the first round.

  • A batch is big when its program holds more than 341 values a thread, which leaves a round under 64 k threads at the
    512 MiB slot budget. Five batches are big:

    batch values a thread threads a round
    KECCAK_RND (the head's widest) 2,763 → 207 8,096 → 108,065
    ECSM 1,782 → 48 12,553 → 466,033
    ECDAS 1,291 → 159 17,327 → 140,689
    KECCAK 859 → 14 26,041 → 1,048,576
    LFM_HASH, in each of the nine W-LFM recursion proofs 365 → 34 61,286 → 657,930
    • KECCAK_RND's first rounds used to run 8,096 threads at 94 ms a launch.
  • Every other batch keeps its program as it was, the byte gate's EQ fixture (26 values) included.

Measured on block 25368371 (FAST, one binary):

A: LAMBDA_VM_ARGUE_LEAN_PROGRAM=0 B: the default Δ
the stage's own ABBA, STARK recursion (965e13de2, wt890–894) 51.75 s (51.7, 51.8) 50.40 s (50.5, 50.3) −1.35 s
at its landing head, pure WHIR (a28ad36af, wt910–914) 45.80 s (45.8, 45.8) 44.40 s (44.4, 44.4) −1.40 s

Where the gain lands: the base.

  • At the landing head the base fell 1.35 s. The big batches' early device rounds went 1,528 → 440 ms, and the argue
    fell 1.28 s.
  • Level 1, the interior and the root each moved +0.00 s. The late rounds held in both runs.
  • LFM_HASH's rounds in the W-LFM proofs went 1,007 → 799 ms of card time, but level 1 is not card-bound. At its start
    2 of 5 programs are ready, and the card waits in the gaps between them, so the saving does not reach the wall.
  • LFM_HASH gains less than the VM's batches because re-reading turns its rounds bandwidth-bound. Its walk reads 1,454
    column values instead of 331, over up to ≈ 4 GB of columns.

Soundness: the proof does not change. The program on demand is the same steps on the same operands in another
order, so every round's values are the same field elements.

  • The transcript, every challenge and the proof's canonical bytes are therefore identical, and so are the program ids
    and the census: equal in every arm of both runs.
  • A cross-check arm (LAMBDA_VM_ARGUE_XCHECK=1) walked the old program in a shadow session over the same card-resident
    values. It compared every big session's rounds, round by round, in the base and in every W-LFM proof, and every one
    matched.
  • Tests:
    • host parity for every VM and W-LFM batch;
    • card parity, round by round, on the five big batches;
    • the whole argument's bytes with the knob off and on, alone and with every other argue knob;
    • a program with every constant off by one is refused by the verifier (BatchMismatch), and by the cross-check
      before a proof exists (DeviceFailed);
    • two mutations fail those checks: one makes the fault inert, the other the comparison.

Opt-out. LAMBDA_VM_ARGUE_LEAN_PROGRAM=0 keeps every batch's program as before and sizes the slot file for one
thread an index, line for line.

The RPX MDS compiled the same way in every build

What changed. Both RPX implementations compute the MDS over a compile-time circulant with plain loops, instead of a
core::array::from_fn closure. The closure's wrapper was inlined only when rustc's codegen-unit partitioning placed it
in mds's own unit; otherwise each output lane was an out-of-line call. That made the host's hashing about 20 % slower
in some builds than in others, decided by unrelated edits.

Measured at 6a6e26611 (two builds, X XF X XF): −0.50 s (45.65 → 45.15 s). The executor's hashing runs at
0.81 of its old time per permutation; most of the gain is in the base (−0.30 s). The STARK PR (#1009), where host
hashing sits on more of the critical path, measured −5.95 s.

Soundness. The same values: the RPO and RPX known-answer vectors, the two implementations' agreement test and a new
test against the circulant's definition pin it, and transposing the matrix fails six of them. No knob.

The card permit after the host prep, and fan-in 4

What changed.

  • The W-LFM prove takes the card permit after its host prep. The prep (the plan's layouts, the columns moved into
    them, the prefix check) is host-only work. It used to run inside the exclusive permit, with the card idle and locked:
    1.68 s of level 1's card holds at job 222. It now runs while another proof holds the card.
  • The pure-WHIR tree went to fan-in 4. Fifteen epochs make four wide level-1 nodes (4 / 4 / 4 / 3 epochs), and
    the block-artifact root takes them directly beside the global wrap.
    • The interior level (2 nodes, 1.8 s) is gone.
    • The root grows from 119 M to 271 M cells, because its HASH table steps to 2^19. It costs +1.2 s: +0.9 s in its
      prove and +0.3 s in its emission, census and build.
  • The other trees keep their arity. The WHIR trees without a wide level 1 (LAMBDA_VM_LFM_PROVER=stark,
    LAMBDA_VM_LFM_WIDE=off) keep fan-in 3, and the STARK tree keeps 2.

Measured on block 25368371 (FAST, one binary per row, arms A B B A):

A/B A B Δ
the permit after the prep, at fan-in 3 (533a22926, wt960–963) 43.70 s (43.7, 43.7) 43.05 s (42.9, 43.2) −0.65 s
fan-in 4 (5f15641b9, wt970–973) 43.95 s (44.0, 43.9) 43.25 s (43.3, 43.2) −0.70 s
both (533a22926, wt980–983) 43.65 s (43.6, 43.7) 42.75 s (43.1, 42.4) −0.90 s
  • Where the time goes with both levers (arm means, from each log's timestamps):
    • level 1 −0.54 s;
    • the interior −1.79 s;
    • the root +1.21 s;
    • the base and the gaps between stages +0.22 s, within the arms' noise.
  • Host peak. The permit after the prep raises it: proves waiting for the card now hold their prepared tables.
    • At fan-in 3: 16.3 → 19.8 GiB.
    • With both levers: 16.41 / 16.21 → 18.63 / 18.78 GiB, ≈ +2.4 GiB.
    • Either way it stays under the 33.5 GiB gate.
  • The ruling.
    • All three A/Bs read MECHANISM MISS · WALL BEYOND SPREAD, so none met the EFFECTIVE rule on its own. Both levers land
      because the wall replicated: the whole-run row hit its pre-registered band in all three, each beyond its A arms'
      spread (0.20 s).
    • Every miss was a secondary prediction:
      • the device work and the prep under contention (+6 … +8.5 % and +31 %, first A/B);
      • the root's census (270.9 M against [140, 245], second A/B);
      • one arm's prep in the combined A/B (−1.5 % against +10 … +50 %).
    • The levers' own mechanisms held in every run: the holds lose the prep, the card's working share of level 1 rises,
      and the tree takes the fan-in-4 shape.

Soundness: nothing a proof commits to changes. The tree's shape does change, and the verifier already takes any
arity.

  • The permit moves no byte. The same programs, proved with the permit before and after the prep, serially and
    three at a time, give the same proofs (card_after_prep_tests). The prep never enters the device layer: every entry
    into it is counted. The first A/B's program ids are equal across A and B.
  • Fan-in 4 is a shape the verifier already handles. Each level-1 node verifies its epochs as before, and the
    root's L2G fold regroups the global wrap's roots exactly as the interior did. The root gates now include pure WHIR's
    root (15 epochs at fan-in 4) in:
    • the honest control;
    • the fixed-size schema;
    • both L2G tamper tests: a moved root, and epochs swapped within and across groups.
  • Proven bits: every chain stays in the analysed family, minimum 130.393 bits, unchanged.
  • VRAM: the W-LFM argue's reservation peaks at 21,754 MiB of the 25,688 MiB budget at fan-in 4, with no
    fallbacks. The root still publishes 180 words.

Opt-outs.

  • LFM_CARD_AFTER_PREP=0 takes the permit before the prep. The proofs are the same.
  • LFM_CENSUS_FAN_IN=3 restores the previous pure-WHIR tree and its nine program ids.

Fan-in 5

What changed. The wide pure-WHIR tree defaults to fan-in 5.

  • Fifteen epochs make three wide level-1 nodes of five epochs each, and the block-artifact root takes them directly
    beside the global wrap.
  • Only the WHIR driver's LFM_CENSUS_FAN_IN bound widens, to 2..=5. The STARK drivers keep 2..=4: nothing above four
    has been costed on them.
  • The stark opt-out and LAMBDA_VM_LFM_WIDE=off keep fan-in 3, and the STARK tree keeps 2.

Measured on block 25368371 (FAST, one binary per row, arms A B B A):

A/B A: fan-in 4 B: fan-in 5 Δ
the decision A/B (8b2896c59, wt1000–1003) 42.55 s (42.7, 42.4) 41.00 s (40.9, 41.1) −1.55 s
at this head (b682091a3, wt1010–1013) 42.85 s (42.7, 43.0) 41.00 s (40.8, 41.2) −1.85 s
  • Where the time goes (the decision A/B): level 1 −0.76 s, the root −0.70 s. At this head the root stage
    falls from 1.9 to 1.2 s.
  • Level 1's census falls from 1,052.8 M to 852.6 M cells, exactly as pre-registered.
    • A wide node's rows are its epochs' wrap rows plus a term linear in its epoch count.
    • Five epochs grow the nodes by only 7 %: every table but LANES stays inside its power of two.
  • The root takes 3 nodes instead of 4. Its hash table (253,082 rows) stays under 2^18, so the root falls from
    270.9 M to 149.6 M cells.
  • Host peak falls from 18.6 to 16.2 GiB (18.9 to 16.2 at this head).
  • The card has 1,346 MiB (1.3 GiB) of margin.
    • The W-LFM argue's reservation peaked at 24,342 MiB of the ledger's 25,688 MiB budget (21,754 MiB at fan-in 4).
      The device peak after the base was 25,586 MiB.
    • A block with heavier epochs would push the reservation over. The refused work then falls back to the host:
      slower, never wrong.
    • The fallback counters cover the commit path and every argue site. The GKR tree's refused reservation has been
      counted since 7650b53c7 (a declined prefetch is not a fallback). The production tree at fan-in 5 reads 0
      refusals and 0 fallbacks.
    • Measure a heavier block before relying on five. LFM_CENSUS_FAN_IN=4 is the opt-out.

Soundness: nothing a proof commits to changes except the tree's shape, which the verifier takes at any arity.

  • Proven bits: every chain stays in the analysed family, minimum 130.393 bits.
  • The artifact keeps its 180 published words.
  • The root gates now include the fan-in-5 root (15 epochs, three nodes) in:
    • the honest control;
    • the fixed-size schema;
    • both L2G tamper tests: a moved root, and epochs swapped within and across groups of five.
  • Small blocks compose at five. One to six epochs, each to a verified root on the fixture: one epoch, one wide
    node at two to five, two nodes at six.

Opt-outs.

  • LFM_CENSUS_FAN_IN=4 restores the fan-in-4 tree and its six program ids.
  • LFM_CENSUS_FAN_IN=3 restores the fan-in-3 tree and its nine program ids.

The WHIR base's head ahead

What changed. The WHIR base computes DECODE's univariate root on a helper thread, beside epoch 0, instead of
before the pipeline starts.

  • The serial head. Before the producer started, the head committed the root on the host (an FFT, a 4× LDE and RPX
    leaves over DECODE's columns), then the prepared DECODE commitment on the device. Only then did epoch 0 execute.
  • Epoch 0 needs neither value to execute.
    • Its preparation reads the root, and now waits for it there.
    • Its prove reads the prepared commitment, which keeps its place on the calling thread. So CUDA still comes up on
      the thread that proves, and the DECODE derivation count stays on that thread.
  • The observer now hears the shared DECODE work from the consumer, just before the first epoch it proves. Before,
    it heard it before the pipeline. The level-1 lead-in reads it at its first claim, in the base's tail.
  • The head is stamped under LAMBDA_VM_BASE_SPLIT=1: its start, the root, the prepared commitment and epoch 0's
    wait for the root.
  • The STARK base's head is untouched. STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009 has its own switch.

Measured on block 25368371 (FAST, arms A B B A; all rows but the last are the decision A/B at d117ffedd,
wt1030–1033):

A/B A: serial head B: head ahead Δ
whole run 40.65 s (40.6, 40.7) 40.10 s (40.1, 40.1) −0.55 s
base 31.30 s (31.3, 31.3) 30.65 s (30.6, 30.7) −0.65 s
epoch 0 starts executing 1.47 s (1.49, 1.45) 0.44 s (0.44, 0.44) −1.03 s
first card commit 2.58 s (2.61, 2.55) 1.85 s (1.87, 1.84) −0.73 s
level 1 7.75 s (7.7, 7.8) 7.75 s (7.8, 7.7) 0.00 s
whole run at its landing head (33232d688, wt1060–1063; A = LAMBDA_VM_WHIR_HEAD_AHEAD=0) 40.90 s (40.9, 40.9) 40.20 s (40.3, 40.1) −0.70 s
  • Where the time goes:
    • The serial head spent 1.09 s on the root, then 0.24–0.29 s on the prepared commitment, all before epoch 0
      executed.
    • Ahead, the root took 1.22 s beside epoch 0 and was done before epoch 0's preparation reached it: the preparation
      waited 0.00 s.
  • Why the first commit gained 0.73 s, not the whole head: epoch 0's pipeline fill took 1.40–1.42 s instead of
    1.10–1.11 s. Its collect, sharing the CPU with the root, grew from 0.32 to 0.52–0.54 s.
  • The gain carries to the end of the base. From epoch 1 on the prover is the bottleneck, so the whole prover
    chain starts earlier.
  • Level 1 does not move. The lead-in starts when the last epoch executes, and that moves with the base.
  • All pre-registered bands held.

Soundness: the proofs are byte-identical by construction.

  • The same values, only earlier. The root is the same pure host function of the ELF and the options
    (commitment_from_elf), computed on another thread. The prepared commitment is unchanged and committed in the same
    place. Epoch 0's preparation receives the same root. Only where and when the root is computed changes, never what
    any proof commits to.

  • The test. the_head_ahead_proves_what_the_serial_head_proves proves a run serial and ahead, under both
    preparation schedules. It compares:

    • the DECODE root and the prepared roots;
    • every epoch's bookend root, shapes, output and register fini;
    • the cross-epoch roots and the touched pages.

    It checks that the ahead bundle verifies, and that the observer hears the shared DECODE work once, with the serial
    head's values, before any epoch is proved. It passed on the CPU build and on FAST's device build.

  • What the test does not compare: an epoch's first group root. Its rows follow HashMap order, so it differs
    between any two proves. The existing preparation-schedule test skips it for the same reason.

  • The tree: the 5 program ids are equal in all four arms and equal to the fan-in-5 landing's.

Opt-out. LAMBDA_VM_WHIR_HEAD_AHEAD=0 restores the serial head, with the same program ids. Any other value stops
the run.

Keep the leaf layers through a futile budget miss

What changed. The WHIR retention no longer evicts its leaf layers for a device reservation they cannot rescue.

  • Before. The retention keeps each commitment's leaf layer on the card from its commit to its opening. When a
    reservation misses the budget, the ledger asks the retention to drop layers until the deficit is covered.
  • The waste. Sometimes the layers together hold less than the deficit. Dropping them cannot let the request
    through: it fails anyway, and each opening that needed a dropped layer hashes its leaves again.
  • Now. Such a miss evicts nothing. A miss the layers can cover evicts them exactly as before, so every reservation
    gets the answer it got without this change.
  • Logs.
    • Each futile miss prints a line: the deficit, what the layers held, and whether they were kept.
    • The retention line counts them: futile misses N (M MiB kept).
    • The run's banner says which way it went: [gpu] WHIR retention: … (the pipeline default) or
      (LFM_WHIR_KEEP_FUTILE=0).

Measured on block 25368371 (FAST, one binary, arms A B B A, bb7f5d8ee, wt1080–1083; B = LFM_WHIR_KEEP_FUTILE=1,
now the default):

A/B A: evict B: keep Δ
whole run 39.95 s (39.9, 40.0) 38.70 s (38.8, 38.6) −1.25 s
base 30.55 s (30.5, 30.6) 29.25 s (29.3, 29.2) −1.30 s
the openings' tree rebuilds 2.42 s 1.03 s −1.39 s
  • Where the time goes.
    • Epochs 3–5, the block's KECCAK-heaviest, rebuilt 0.55 s of trees each.
    • With their layers kept, they rebuild 0.08–0.09 s each, the same as epochs 6 and 13, which have the same shape.
    • Level 1 +0.05 s, the root 0.00 s.
  • What was lost before.
    • One futile miss in each of epochs 3–5 took every layer the epoch held (800 / 816 / 816 MiB).
    • Each was for a request that missed by 5,204 / 5,404 / 5,235 MiB, so it failed anyway.
    • Fifteen layers (2,432 MiB) were hashed again at the openings: 519 leaf passes against 504.
  • The reserved margin shrinks from 1,346 to 654 MiB; the margin before a fallback does not move.
    • The reserved peak. The run's highest reservation used to be level 1's argue: 24,342 MiB of the 25,688 MiB
      budget, 1,346 MiB of room. It is now the base's epoch 4 argue: 25,034 MiB, 654 MiB of room. Epochs 3 and 5 peak
      at 24,833 and 24,865. Each rose by exactly the layers it kept.
    • Before a fallback, nothing moves. The kept layers (816 MiB in epoch 4) give way to any request they can cover,
      so a request there fails only when it is more than 1,470 MiB short. That was the threshold before too.
      Level 1's argue still peaks at 24,342 MiB, unchanged in every arm.
    • The device peaks at 28,466 / 28,082 MiB in the base (32,607 on the card), against 27,634 / 27,282.
    • A heavier block that needs more than the 654 MiB goes back to the old eviction. It happens one reservation at a
      time: a request the layers can cover evicts them and succeeds, as before this change. That epoch pays the old
      re-hash and nothing more, so this change never refuses what the old code granted.
    • A request that misses by more than the layers fails with or without this change. Its work falls back to the
      host, counted: device fallbacks and GKR tree refusals on the argue, commit fallbacks on the commit path. Slower,
      never wrong.

The trigger, likely but not confirmed.

  • What the run shows. Each miss came 0.08–0.26 s into its epoch's argue (about 1 s long) and was short by
    5.2–5.4 GiB.
  • The likely requester is the GKR input-layer tree's eager probe (multilinear::gpu::input_layer_tree_impl,
    reserve(eager)):
    • it asks for the table's whole carry, 4 × slots × rows × 24 bytes, and drops the reservation at once;
    • on a refusal it builds the lazy tree, still on the card, and counts nothing.
  • Why nothing else fits:
    • every counted fallback read 0;
    • the argue's other silent request cannot miss: the prefetch checks the free budget before it reserves;
    • the group's room is reserved at the commit, 7.5 GiB under the budget.
  • Not confirmed: the miss line reports the deficit, not the caller.

Soundness: nothing a proof commits to changes.

  • What the change touches. It decides whether an opening copies a kept leaf layer or hashes the leaves again. The
    tree, its root and its paths are the same either way.
  • The card test. a_miss_the_layers_cannot_cover_keeps_them_and_moves_no_path takes a futile miss both ways on the
    card. It compares the root and the paths with a fresh codeword's.
  • The A/B. The five program ids were equal in all four arms, and every root verified at 180 words.
  • The gates prove the production tree at the default and under =0, and diff each tree's five program ids
    against WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's.

Opt-out. LFM_WHIR_KEEP_FUTILE=0 evicts as before. Any value other than unset, 0 or 1 stops the run.

Keep whole WHIR trees on the card, evictable

What changed. A commitment now keeps its whole Merkle tree on the card, from its commit to its opening. Before, it
kept only the leaf layer.

  • Before. The retention kept each commitment's leaf layer from its commit to its opening. Every opening rebuilt
    the inner levels from it: one permutation a node, level by level. On the block that cost the openings 1.02 s of tree
    rebuilds over 612 opens.
  • Now. The node array the commit built is kept whole. An opening with a matching kept tree reads its paths from
    it, builds nothing and hashes nothing.
  • Where the bytes sit. They sit where the leaf layer's did: grown into the codeword's device reservation, and
    handed to the same evictor. A whole tree costs one more layer's bytes, num_leaves − 1 nodes.
  • If the budget refuses the tree at the capture, the leaf layer is kept instead, as before.
  • Logs.
    • The banner: [gpu] WHIR retention: whole trees, the inner levels kept with the leaves (the pipeline default), or
      leaf layers, … (LFM_WHIR_WHOLE_TREES=0).
    • The retention line ends with whole trees on (N openings served).
    • A covered eviction's line counts whole trees and leaf layers separately.

Measured on block 25368371 (FAST, one binary, arms A B B A, 22b81fc63, wt1084–1087). A = the keep-futile
landing's default (leaf layers). B = LFM_WHIR_WHOLE_TREES=1, now the default.

A/B A: leaf layers B: whole trees Δ
whole run 38.80 s (38.8, 38.8) 37.80 s (37.6, 38.0) −1.00 s
base 29.20 s (29.2, 29.2) 28.30 s (28.2, 28.4) −0.90 s
the openings' tree rebuilds 1.02–1.03 s 0.20 s −0.82 s
  • Where the time goes.
    • 952 openings were served a kept tree. Leaf passes stayed at 504–506, and trees built fell from 1,458 to 506: one
      per commit, plus the two evicted trees.
    • Every epoch but epoch 4 rebuilds 0.00 s of trees. Epoch 4 rebuilds 0.18 s: its argue evicted two kept trees
      (544 MiB), and their openings built them again.
    • Level 1 −0.05 s, the root −0.05 s.
  • The model was −0.76 s of tree rebuilds and −0.85 s on the whole run. Both landed inside their pre-registered
    bands.

VRAM: the reserved margin is gone in the heavy epochs. The margin before a fallback does not move.

  • The argue's peaks. Epochs 3–5 are the block's KECCAK-heaviest. Their argues peak at 25,633 / 25,306 / 25,681 MiB
    of the 25,688 MiB budget. Epoch 5 comes within 7 MiB of the budget. Epoch 4 would reach 25,850 MiB: it evicts two
    kept trees and peaks at 25,306.
  • Why that is safe: everything kept is evictable.
    • The kept trees give way to any request they can cover. That request succeeds, and the epoch pays the old rebuild
      for the trees it took.
    • A request they cannot cover fails with or without them. It falls back to the host, counted: device fallbacks and
      GKR tree refusals on the argue, commit fallbacks on the commit path.
    • So the worst case on a heavier block is today's rebuild, not a new fallback. The point where a request actually
      falls back is where it was before the change.
  • The measurement. Every fallback counter read 0 in all four arms, and every argue peak stayed ≤ 25,688 MiB.
  • The simultaneous footprint of kept bytes doubles, 833 → 1,666 MiB.
  • The device peaks at 28,818 / 29,234 MiB in the base window (32,607 on the card), against 28,658.
  • One transient under-count, bounded. An opening served a kept tree lets the slot's lock go while it reads its
    paths. An eviction in that window gives the tree's bytes back to the ledger while the buffer lives on until the path
    gather ends. That is about 0.1 ms and at most one tree (≤ 512 MiB), and it falls inside the ≈ 3.4 GiB of card left
    above the device peak.

H4, and why this is not it. H4 kept whole trees and lost about 15 s. Its trees sat outside any reservation, so a
full card made device allocations fail, and commits fell back to the host at about 1.5 GiB a chain. These trees are
reserved and evictable, and the tests now pin that invariant rather than "no tree is ever kept":

  • The group guard (a_group_holds_only_its_codewords_before_any_open) runs in both modes. It checks three things:
    • four unopened commits keep exactly the object the mode names;
    • the ledger grew by exactly four codewords plus those four objects;
    • the driver holds less than one tree plus one codeword beyond what was promised. Four trees kept outside the
      promise would read four trees there.

Soundness: nothing a proof commits to changes.

  • What the change touches. It decides whether an opening reads a kept tree or builds the same tree again. The
    root and the paths are the same either way.
  • The card test. a_kept_whole_tree_serves_its_openings_and_moves_no_byte:
    • The reference is leaf layers: the root, the paths, and the paths with a cap of height 3.
    • With whole trees, two openings are served with no build and no leaf pass, byte-equal to the reference.
    • A covered reserve then evicts the tree; the next opening builds it again, gives equal paths, and keeps it whole.
  • Both modes. Every retention card test whose counts or bytes depend on what is kept runs both ways: the counts,
    the reservation, the blocking key, the process-wide identities, and the served / rehashed / evicted cap regimes.
  • The A/B. The five program ids were equal in all four arms, and every root verified at 180 words.
  • The gates prove the production tree at the default and under =0, and diff each tree's five program ids
    against WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's.

Opt-out. LFM_WHIR_WHOLE_TREES=0 keeps leaf layers as before. Any value other than unset, 0 or 1 stops the run.

A kept tree's promise given back by its last handle

What changed. A kept whole tree now owns its ledger promise: the bytes go back to the device ledger when the
tree's last handle drops, not when its slot is emptied. An eviction or a codeword's drop while an opening still reads
the tree gives nothing back until that opening lets go, and the evictor passes over a tree an opening is reading
instead of counting it as reclaimable. This makes the transient under-count disclosed in the whole-trees section
unreachable. A request only a tree being read could cover now fails, counted, instead of being granted bytes the card
still holds; the window is one served opening's read.

  • Logs. An eviction line appends · N kept tree(s) passed over: an opening was reading them when it happens.
  • Cost. No hot path: per served opening one reference-count clone, per capture one more, per eviction walk one
    count read per entry. No timing A/B; the gates' production tree read as whole trees' (952 openings served, 2 trees
    evicted, 3 futile misses kept, 0 passed over, the same five program ids).
  • Test. On the card, a_kept_tree_an_opening_reads_stays_promised_until_the_opening_lets_go holds a reader
    across a miss only its tree could cover, then across its codeword's drop, and checks the ledger at each step; two
    mutations (the walk ignoring readers, the promise back with the slot) each fail it.

The argue's zerocheck computed the stage-1 way: same messages, a quarter of the device time

What changed. Each table's zerocheck batch — eq(r,x)·C(x) + eq(ρ,x)·(γ·N(x) + γ²·D(x)), the constraint rule
beside the bus's two — is summed a different way on the card. Every round sends the message it sent before
(D-ARGUE §2.7: a round polynomial is a function of the batch, the challenges drawn before it and the round, so any
exact algorithm for it sends the same message).

  • The bus as one column (S1-2). Both bus rules are affine in the factors with constant coefficients, so
    γ·N + γ²·D is one column L = a₀ + Σ a_k·f_k, built once per table after the batching challenge. KECCAK_RND's
    2,062 interaction sides leave the per-row walk.
  • The weights pulled out (S1-4, Gruen). Round j is E^r·eq₁(r_j,X)·A_j(X) + E^ρ·eq₁(ρ_j,X)·B_j(X): the card
    walks the constraint part alone, at X ∈ {0, 2..d} under eq(r_{>j}), and sums L's two halves under
    eq(ρ_{>j}); A_j(1) comes from the claim the round carries in.
  • Rounds 0 and 1 in the base field (S1-5). One pass over the 4-row groups evaluates the constraint part on the
    grid {0..d}² in Goldilocks — each factor's grid by additions from its group's four rows — and both messages come
    out of it; the grid's four boolean corners are zero on a trace that satisfies its AIR and are not computed. Both
    folds then run at once, base to extension, into a quarter-size copy.
  • Integer nodes (S1-1). An interpolation node is (k, 0, 0), so t·(hi − lo) is the componentwise k·(hi − lo):
    three products where the full multiply spends nine — in every device round, GKR and reduce included.
  • Where. Kernels zc_grid01, zc_bus_u, zc_fold2, zc_bus_column, zc_halve, zc_round_gruen,
    sumcheck_round_ext3_int (math-cuda/kernels/sumcheck.cu); host math_cuda::argue_fused, multilinear::gpu_fused
    (the constraint program with an ACC step after each root), batch::prove_resident_with. The host reference is
    multilinear::fused, which the card is checked against. The rounds stop at today's host crossover (cube 32) and
    hand today's host tail the same factors.
  • Logs. ★ ARGUE FUSED: on (the default; LAMBDA_VM_ARGUE_FUSED=0 is today's rounds) and [gpu] sumcheck rounds: integer nodes (the default; …) once a run; each epoch's ARGUE ZEROCHECK #k line ends || fused F (declined D) · M ms.

Measured on block 25368371 (FAST, one binary, at 9e27289 + the change, d52f9dff6; A = today's rounds, B =
fused + integer nodes, now the default). Two A B B A jobs, the second a replication decided pooled with the first:

arm whole run base level 1 argue (Σ epochs) zerocheck device argue reserved HW
A (job 274: wt1097, wt1100; job 270: wt1101, wt1104) 40.2 / 40.2 / 40.2 / 40.3 s 30.6 / 30.6 / 30.6 / 30.7 s 8.0 / 8.0 / 8.0 / 8.0 11.69 / 11.70 / 11.69 / 11.66 s 4.02–4.03 s 24,218 MiB
B (274: wt1098, wt1099; 270: wt1102, wt1103) 37.4 / 37.4 / 38.0 / 37.1 s 27.9 / 27.8 / 27.7 / 27.9 s 7.8 / 8.0 / 8.6 / 7.7 8.86 / 8.64 / 8.68 / 8.79 s 1.06–1.07 s 24,741 MiB
B − A, pooled 4 + 4 −2.75 s −2.80 s +0.03 −2.94 s −2.96 s +523 MiB
  • Every B arm's base is below every A arm's. Job 274 alone read −2.80 s whole / −2.75 base / −2.95 argue; the
    replication −2.70 / −2.85 / −2.94. wt1102's level 1 (8.6 s) is a single-arm outlier of the kind G6-LEDGER §9
    documents; its whole run minus level 1 equals its twin's (29.4 s).
  • Where the time goes. The zerocheck's device rounds fell from 4.03 to 1.07 s a run. The GKR's device rounds moved
    −0.06 s (integer nodes only; the GKR keeps today's round kernel — stage 1's S1-3 is not in this change).
  • Per table (the cross-check arm, FAST job 273 wt1092, each fused table's rounds and today's timed over the same
    factors in one process): KECCAK_RND 1,279 → 258 ms (R 0.20), CPU 1,094 → 288 ms (0.26), LFM_HASH 680 → 385 ms
    (0.57); every fused table together 5,045 → 1,624 ms (0.32). Small and mid tables gained too: today's per-round cost
    was mostly the walk of the whole batch, not launch latency.
  • 390 tables a run take the fused rounds; 3 decline to today's (KECCAK at 2^6 rows — the fused rounds need 2^7).

At f3d359998 (FAST, eight arms A B B A A B B A, wt1105–1112; A = LAMBDA_VM_ARGUE_FUSED=0 LAMBDA_VM_ARGUE_INT_NODES=0, B = the defaults). The base and the argue decide; level 1 is reported:

A/B A: today's rounds B: stage 1 Δ
whole run 37.80 s (37.7, 37.7, 37.8, 38.0) 35.95 s (36.2, 35.7, 35.5, 36.4) −1.85 s
base 28.27 s 26.05 s −2.22 s
argue (Σ epochs) 11.65 s 8.67 s −2.98 s
zerocheck device 4.03 s 1.07 s −2.97 s
the openings' tree rebuilds 0.20 s 0.72 s +0.52 s
level 1 7.88 s 8.35 s +0.47 s
  • Every pre-registered band held. The argue gains what it gained at 9e27289 (−2.98 against −2.94 s).
  • The whole-tree retention pays part of it back. The fused session reserves 523 MiB beside the lifted factors (a
    quarter-size folded copy, L, two weights and its slot files). At that head the heavy epochs' argues already sit at
    the budget with evictable kept trees, so the reservation takes them: each B arm evicted 10 kept trees instead of 2,
    served 947 openings a kept tree instead of 952, and rebuilt 0.52 s more of trees. The argue's reserved peak read
    25,660 MiB of 25,688 (A 25,681). Every fallback counter read 0 in all eight arms.
  • Level 1 +0.47 s is the spread of the B arms (8.7 / 8.0 / 7.9 / 8.8 s against 7.8 / 7.7 / 7.8 / 8.2): two B arms
    carry +0.8 s single-arm outliers of the kind G6-LEDGER §9 documents. Stage 1 acts on the base; the whole run minus
    level 1 moved −2.32 s.
  • Host peak 16.4 GiB (A 16.2), device peak 29,266 MiB in both settings.

Soundness: nothing a proof commits to changes.

  • Every message is today's, by construction: each technique is a polynomial identity in exact arithmetic
    (D-ARGUE §2.7), and the only case where they would differ — a trace that breaks its AIR, under the corner skip —
    gives a proof the verifier rejects either way.
  • The cross-check arm. With LAMBDA_VM_ARGUE_FUSED_XCHECK=1, today's device rounds replay the fused challenges
    over the same factors and every message is compared, with the factors at the crossover, and the grid's corners are
    checked per row: on the block, 390 of 390 fused tables confirmed, the root proved and verified (job 273, wt1092).
  • Canonical bytes. stark::multilinear_table::tests::the_argument_proves_the_same_bytes_with_the_fused_rounds
    proves the tall tables with the fused rounds, alone and with the other argue knobs, and compares the canonical proof
    bytes table by table and whole, the transcript and the next challenge with today's; it runs on the card in the gates.
  • On the card (prover::tests::argue_stage1_tests, run alone): the fused rounds against today's host rounds on
    every VM table and W-LFM chip, the three heaviest at 2^10 and 2^12 (42 runs, each confirmed by the replay); the
    twelve tables without roots under the production switches; test_keccak proved with the fused rounds and verified,
    with and without the cross-check; the integer-node rounds.
  • Negative controls, each with a mutation that makes its check inert and must redden it: a wrong bus coefficient
    on the card is refused by the cross-check; a trace that breaks its AIR is refused by the corner check; a wrong bus
    coefficient in a proof parts from today's at the first table and does not verify.
  • The A/Bs. The five program ids were equal on all sixteen arms (= WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's), every root verified.
  • A bug the A/B caught and the tests now cover. The first timing (job 273) crashed in the prover on a table
    without constraints (its grid is all corners, so with the corners skipped no grid point was left, and the launch
    sizing divided by zero). A prover panic, not a wrong proof; fixed before any number above was taken.

Gates. FAST2 job 177 at land/1010-argue-q1b (f3d359998): 16 steps, all green — the lib suite 1,640 / 0 /
100, math-cuda 279, stark 420. Besides the standard steps: the defaults pinned on the host; the card tests (42 fused
runs, the twelve rootless tables, test_keccak proved and verified both ways, integer nodes) and both mutations red;
real traces under the cross-check, none parted; -p multilinear --features cuda --lib (368) and -p stark --features cuda,multilinear/cuda --lib multilinear_table (33, the canonical-bytes test on the card among them); the production
tree at the defaults (298 fused sessions over the base epochs, 947 openings served, #1010's five program ids) and under
both opt-outs (no fused session, the same ids).

Opt-outs. LAMBDA_VM_ARGUE_FUSED=0 runs today's zerocheck rounds exactly; LAMBDA_VM_ARGUE_INT_NODES=0 today's
round kernel. Tables are also filtered by committed width with LAMBDA_VM_ARGUE_FUSED_WIDTHS (unset: every table).

The argue's GKR layers with Gruen's split (M1-1): same messages, half the GKR time

What changed. A device GKR layer's relation is eq(u,x)·h(x), h = p_lo·q_hi + p_hi·q_lo + λ·q_lo·q_hi. Round
j sends s(t) = E·eq₁(u_j,t)·H(t), with E = Π_{i<j} eq₁(u_i,s_i) and H(t) = Σ_{x'} eq(u_{>j},x')·h(s_{<j},t,x')
a quadratic (the product form of eq). The card now sums the layer the Gruen way:

  • Two sums a round, in registers. The round kernel sums H(1) and H(2), the factors extended by additions
    (2·hi − lo), with no eq factor in the walk and no program interpreter or slot file. The host forms
    s(1) = E·u_j·H(1), takes s(0) = claim − s(1), so E·H(0) = s(0)/(1 − u_j), and extrapolates
    H(3) = H(0) + 3·(H(2) − H(1)). Where 1 − u_j has no inverse, the card sums H(0) too.
  • The fold rides the next round. Round j's pass first binds the halves to s_{j−1} (reads four cells a factor,
    writes two) and then sums. No separate fold launch, and one read of each level fewer.
  • No layer-sized eq table. The weight is eq(u_{j+1..J−1})[x >> L]·eq(u_{J..})[x mod 2^L]: one small table per
    card round plus the tail's, all built by one launch a layer. Before, each layer built a 2^m table (m launches)
    and folded it every round.
  • The host tail from a cube of 64 (it was 512): a Gruen round is one launch and one read-back, cheaper than a
    host round over more than a few dozen pairs. The tail gets the factors it got before, eq included, at the smaller
    cube.
  • Where. Kernels gkr_eq_levels_ext3, gkr_round_gruen, gkr_gruen_finish (math-cuda/kernels/sumcheck.cu);
    host math_cuda::gkr::GruenLayer (tree layers and the rebuilt input layer), multilinear::gkr_gruen (the knobs,
    each round's message from the card's sums, a host reference of the whole device algorithm),
    gpu::DeviceTree::prove_layer_gruen.
  • Logs. ★ ARGUE GKR GRUEN: on (the default; LAMBDA_VM_ARGUE_GKR_GRUEN=0 is the old layer rounds; …) once a run;
    each epoch's ARGUE GKR #k line ends || gruen G (xchecked X).

Measured on block 25368371 (FAST job 330, one binary at 1946609c3 = f3d359998 + the change behind its knob,
eight arms A B B A A B B A, wt1402–wt1409; A = the defaults then, B = LAMBDA_VM_ARGUE_GKR_GRUEN=1, now the
default). The base and the argue decide; level 1 is reported:

A/B A: the old layer rounds B: Gruen's Δ
whole run 36.42 s (36.2, 36.6, 36.5, 36.4) 34.85 s (35.0, 35.3, 33.9, 35.2) −1.57 s
base 26.07 s 24.30 s −1.77 s
argue (Σ epochs) 8.66 s 6.58 s −2.08 s
GKR total (device layers: rounds, between layers, values, rebuild) 4.24 s 2.13 s −2.11 s
of it, device rounds 2.95 s 1.70 s −1.26 s
of it, host work between layers 1.02 s 0.25 s −0.77 s
level 1 8.75 s 9.03 s +0.28 s
  • Every pre-registered band held (argue −2.0 [−2.8, −1.2], base −1.9 [−2.8, −1.0], whole −1.8 [−2.8, −0.8], the
    rounds' slot 0.9–1.8 s).
  • Where it went. The device rounds went from ≈ 22.3 k to ≈ 32.4 k a run (the lower crossover), and from ≈ 132 to
    ≈ 52 µs a round. Between layers, the host tail fell 657 → 170 ms and the session set-up with the eq table.
  • Level 1 +0.28 s is the B arms' spread (9.1, 9.4, 8.1, 9.5 s against 8.7, 8.7, 8.7, 8.9 s). M1-1 acts on the
    base.
  • Memory. The argue's reserved peak read 25,660 MiB in both settings; tree rebuilds 0.72 → 0.71 s; evicted kept
    trees 10 → 12.2 a run; device peak 29,266 MiB (A up to 29,298). The host peak read 19.9, 19.7, 16.3 and 20.0 GiB in
    the B arms against 16.3 GiB in every A arm. M1-1 allocates nothing on the host; why the peak rose in three arms is
    not established (the base now ends ≈ 1.8 s earlier against the same level-1 work).

Soundness: nothing a proof commits to changes.

  • Every message is today's, by construction. The product form of eq and distributivity move no value, the
    extrapolation is exact for a quadratic, and s(0) + s(1) = claim holds for today's own messages whenever a layer is
    the fold of the one below, which it is for a tree the card folded. The tail receives today's factor values.
  • The cross-check arm. With LAMBDA_VM_ARGUE_GKR_GRUEN_XCHECK=1, today's layer program walks the same folded
    halves beside every Gruen round, with its own eq table folded on the same challenges; every message and the five
    factors at the crossover are compared, and a difference fails the prove. On the block (job 330's X arm), 3,381 of
    3,382 layers compared equal, and the root proved and verified. One layer was not shadowed because the card had no
    room for today's eq table beside it; it was counted, not skipped silently.
  • Canonical bytes. stark::multilinear_table::tests::the_argument_proves_the_same_bytes_with_the_gkr_gruen_rounds
    proves the tall tables with Gruen's rounds (alone, with the other argue knobs, and beside the fused zerocheck) and
    compares the canonical bytes table by table and whole, the transcript and the next challenge; it runs on the card in
    the gates.
  • On the card (multilinear/tests/argue_gkr_gruen.rs): today's tree against Gruen's rounds at 2^14, 2^16 and
    2^20 inputs, host tails from cubes of 512, 64, 32 and 2, and with H(0) summed on the card: 15 proofs equal field
    element by field element, with the same transcript.
  • The host reference (multilinear::gkr_gruen::host_rounds) runs the device algorithm on the host and equals
    today's rounds, challenges and factors at every layer size 2^1–2^9 and every split of card rounds and host tail,
    including coordinates of one; three mutations of its arithmetic each fail it.
  • Negative controls: a wrong round (one added to s(1)) makes the proof differ at the first table and fail GKR's
    layer check (LayerRelationMismatch), and is refused by the cross-check; with the cross-check's comparison made
    inert, that test fails.
  • The A/B. The five program ids were equal on all nine arms (= WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's), every root verified.

Gates. FAST job 330 at 1946609c3: the card suite, the host reference, the tall tables' bytes and fault, and the
mutation. FAST2 job 200 at 7d82a320f: 15 steps, all green (see Gate and CI).

Opt-outs. LAMBDA_VM_ARGUE_GKR_GRUEN=0 runs the old layer rounds exactly. LAMBDA_VM_ARGUE_GKR_GRUEN_TAIL=<cube>
moves the crossover (a power of two, default 64).

The argue's GKR input layer from the base columns, and no lift (M1-2): same messages, half the tree time

Where the time was. Split per region (ARGUE REST, ARGUE TREE under LAMBDA_VM_BASE_SPLIT), the argue's
"rest" was mostly the GKR tree region: 1.80–1.98 s a block, of which the card spent 1.42 s writing the input layer
(FAST job 332, each part waiting for its own kernels). Each table lifted its columns into the extension (24 B a cell)
and ran two program launches an interaction over them, ≈ 38 k launches a block, each with its own slot file.

What changed.

  • The input layer from the base columns (M1-2a). One launch a table writes every interaction's two sides,
    constant + Σ coeff·column[row + shift], as base × extension products straight from the epoch's resident columns,
    from a plan built once a table and kept for the layer's rewrite (gkr_input_from_columns, logup::input_plan). The
    same cells, so the same tree, output and GKR proof.
  • No lift (M1-2b). With the tree written from the columns, the zerocheck's first pass was the lifted factors' last
    reader, and it reads only their base values. The fused kernels now take a row stride (3 for lifted factors, 1 for base
    columns) and read the columns where they lie; a public table is a base copy. The W·rows·24 B lift is made only when
    the old rounds need it: the fused rounds declined (three KECCAK tables at 2^6 rows a block) or the cross-check replays
    them. A table with a shifted factor would be lifted as before (none in production).
  • Logs. ★ ARGUE GKR INPUT: from the base columns (the default; …) and ★ ARGUE NO LIFT: on (the default, …) once
    a run; each epoch's ARGUE TREE #k line ends || from the columns C · no lift N (late L).

Measured on block 25368371 (FAST, one binary each, eight arms A B B A A B B A). The base and the argue decide; level 1
is reported:

A/B A B Δ
M1-2a (job 333, 87ed8dbcd; A = the defaults then, B = LAMBDA_VM_ARGUE_GKR_INPUT=1): whole 34.35 s 33.62 s −0.73 s
base 24.25 s 23.50 s −0.75 s
argue (Σ epochs) 6.55 s 5.58 s −0.97 s
tree region 1.98 s 1.11 s −0.86 s
M1-2b (job 335, ba784bd0b; A = M1-2a, B = + LAMBDA_VM_ARGUE_NO_LIFT=1): whole 34.17 s 32.62 s −1.55 s
base 23.32 s 22.43 s −0.90 s
argue (Σ epochs) 5.49 s 5.14 s −0.34 s
argue reserved peak 25,660 MiB 24,153 MiB −1,507 MiB
kept WHIR trees evicted a run 13 2.2
the openings' tree rebuilds 0.74 s 0.04 s −0.70 s
level 1 9.30 s 8.62 s −0.68 s
  • In each A/B every B arm's base (M1-2a) and whole run (M1-2b) is below every A arm's. Every pre-registered band of
    M1-2a held; M1-2b's were missed on the beneficial side. It had been sized on the lift's own card time (0.22 s); the
    ≈ 1.5 GiB the lift held was what the heavy epochs' argues took from the kept WHIR trees, so with it free they stop
    evicting them, and the openings stop rebuilding them. Level 1's −0.68 s is not attributed.
  • Device peak 26.4–27.1 GiB with no lift, against 28.9–29.2 GiB without.

Soundness: nothing a proof commits to changes.

  • The same cells and rounds. The input layer's cells are the same field values (an affine combination evaluated
    over base columns instead of their lifts), and the fused rounds read the same base values through a different stride.
  • The plan's cells equal logup::input_layer over the lifted factors for direct and shifted columns, padded and
    not (a host test; a mutated shift fails it), and a side reading a public factor has no plan.
  • On the card (multilinear/tests/argue_gkr_input.rs): the columns' tree proves the old tree's proof, output and
    transcript at four shapes, carried and handed back (its rewrite); a wrong plan constant moves the output.
  • Canonical bytes. stark::multilinear_table::tests::the_argument_proves_the_same_bytes_with_the_input_from_columns
    proves the tall tables with the input from the columns alone, beside the production defaults, and with no lift (its
    tables reading the columns), against the canonical bytes, transcript and next challenge of the defaults without.
  • The fused card tests (prover::tests::argue_stage1_tests, run alone) pass through the strided kernels, and
    test_keccak proves and verifies under the new defaults.
  • The A/Bs. The five program ids were equal on all sixteen arms (= WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's), every root verified.

Opt-outs. LAMBDA_VM_ARGUE_GKR_INPUT=0 writes the input layer from the lifted factors (and then no lift is off
too); LAMBDA_VM_ARGUE_NO_LIFT=0 lifts every table's factors.

Level 1: the global child proves after the last wide node's artifacts

What changed. In a wide level 1, the WHIR global child's multi_prove now waits on a card latch until the last
wide node (L1N2 on the block) has built its artifacts.

  • Before. The card was first come, first served. Level 1 ends when L1N2 proves, and L1N2's chain is its
    prologue, its artifacts, its host prep (≈ 1.1 s), then its prove. When the global asked for the card first, L1N2's
    artifacts waited out the global's whole prove (0.4–0.9 s), and L1N2's host prep then ran with the card idle.
  • The race. Over 34 earlier arms (jobs 246–292), the global went first in 11, and all 11 read level 1 at 8.0–8.7 s.
    No arm under 8.0 s had the global first.
  • Now. The global keeps its host prep where it was; only its card request moves behind L1N2's artifacts. Its prove
    then runs during L1N2's host prep, where the card was idle.
  • Bounded, and never a deadlock.
    • Only multi_prove waits, only on an armed permit, and at most 30 s. After that it queues anyway, counted, with a
      line saying so.
    • The node that opens the latch also opens it if its task unwinds.
    • The latch is made only when every task of the level has a worker (siblings ≥ tasks); otherwise the lever prints
      INACTIVE with the reason, and nothing waits. A level without the global in its pool (wraps,
      LFM_TREE_TOP_OVERLAP=0, the fixture tree) also makes no latch.
  • Logs.
    • The banner, once a level: ★ GLOBAL AFTER LAST: the WHIR GLOBAL child's prove waits until L1N2 has built its artifacts (on by default; LFM_TREE_GLOBAL_AFTER_LAST=0 turns it off), ★ GLOBAL AFTER LAST OFF: …, or
      … requested but INACTIVE: <reason>.
    • One CARD DEFER #n multi_prove: waited …s for its latch line for the global.
    • Under LFM_CARD_TRACE=1, each CARD HOLD / CARD DEFER line ends · who=<proof> (global, L1N0…, wrap k), so
      the card order is read from the log rather than inferred from timing.

Measured on block 25368371 (FAST job 255, one binary at 7d82a320f + the lever, before M1-2, arms A B B A × 2,
wt1520–1527; A = off, B = LFM_TREE_GLOBAL_AFTER_LAST=1, now the default):

A/B A: off B: latch Δ
whole run 34.58 s (35.0, 34.6, 33.7, 35.0) 34.15 s (34.4, 34.1, 33.8, 34.3) −0.43 s (SE 0.33, t −1.27)
level 1 8.80 s (9.3, 8.7, 7.9, 9.3) 8.40 s (8.6, 8.4, 8.2, 8.4) −0.40 s (SE 0.34, t −1.17)
base −0.05 s
the global first on the card 3 of 4 arms 0 of 4
  • ⚠ Noise. Four arms a setting, and level 1's spread in A is wide (7.9–9.3 s), so both deltas sit at about one
    standard error (t ≈ −1.2). Both landed inside their pre-registered bands (level 1 [−0.60, −0.10], whole
    [−0.60, −0.05]), but this run alone does not separate −0.4 s from a smaller gain.
  • What the latch does and does not do. It removes the global-first tail: A's three global-first arms read 8.7–9.3 s
    and B has none. It does not shorten L1N2's prologue (≈ 4.8–5.3 s in both settings), which is now level 1's floor; A's
    one arm without the race (7.9 s) is faster than every B arm (8.2–8.6 s).
  • Earlier runs of the same lever (at the previous head, before the GKR Gruen rounds shortened the base): job 252 read
    level 1 −0.19 s and whole −0.12 s with the race gone in every latched arm; its replication (job 253) read whole
    −0.04 s. Since then the GKR Gruen rounds shortened the base by ≈ 1.8 s while the wide nodes' prologues start at the
    same point (I-GKR's reading), so the global reaches the card first more often and the latch has more to remove: the
    global went first in 3 of 4 unlatched arms here, against 4 of 8 in job 252 and 2 of 8 in job 253.
  • Not re-timed on top of M1-2. M1-2 moved level 1 by −0.68 s itself (not attributed), so how often the global
    still wins the race at 88b0d3196 is unmeasured. Where the race does not happen the latch is already open when the
    global asks, but a race-free arm is not proven free: job 252's latched arms sat 0.05 s above its race-free unlatched
    ones, and here A's one race-free arm (7.9 s) beat every latched arm (8.2–8.6 s).
  • Host peak (reported, not attributed). A 18.03 → B 18.93 GiB. The peak is bimodal in both settings, 16.2–16.3 or
    19.7–19.9 GiB (A 2 of 4 arms high, B 3 of 4), so eight arms cannot attribute the +0.9 GiB to the latch.

Soundness: nothing a proof commits to changes. The latch moves one card request in time. The five program ids were
identical in all eight arms of job 255, and every root verified at 180 words. The gates below prove the production tree
at the default, under =0 and with too few workers, and diff each tree's five ids against job 255's (= #1010's).

Opt-out. LFM_TREE_GLOBAL_AFTER_LAST=0 restores first come, first served. Any value other than unset, 0 or 1
stops the run.

WHIR grinding at 18 bits, 114 queries

What changed. Every WHIR chain, in the base and in every recursion proof, now grinds 18 bits before its query
positions instead of 20, and opens 114 queries instead of 112 to buy the two bits back. It is one setting,
multilinear_prove::chain_config_under, behind the knob LAMBDA_VM_ZF_WHIR_GRIND_BITS=20|18 (default 18). Commits:
fc0ff59 (the knob), 329b9c6 (an ignored per-height census of the chain verifier at 20 and 18), 5364aa2 (the
default flip), 2687e48 (the W-leg sizing test pins follow the process's bits).

Measured (FAST, one binary, eight arms A B B A A B B A, wt1660–1667; A = =20, B = 18):

A: 20 bits, 112 queries B: 18 bits, 114 queries Δ
base 22.32 s 21.75 s −0.57 s (every B base under every A base)
grind time, summed over the run 1.39 s 0.60 s −0.79 s
whole block −0.20 s
level 1 +0.40 s

The grind work fell 2.3× rather than the 4× that two fewer bits alone would give: each grind keeps a fixed launch and
readback cost.

Soundness: no proven bits are lost.

  • The chain minimum stays 130.393 bits at stack 27, set by the first fold, which is unground either way. The shift
    phase moves from 130.926 to 130.907 bits.
  • The minimum is 130.393 bits at every production height, n = 15–27 (calculator security/zisk_calc.py).
  • The grind bits and the query count are verifier-side constants absorbed in the statement, so a proof made at one
    setting does not verify at the other.
  • Recursion: the two extra queries per chain grow the root's LFM_HASH table from 253,082 to 258,602 rows. It stays
    padded to 2^18, with 3,542 rows (1.35 %) of headroom.
  • Gates (FAST job 667): the full lib suite (1653 passed, 0 failed); guests, math-cuda, the RPX parity, stark and
    crypto suites; the production tree at 18 bits (banner 18, q = 114) and at =20, each matching its reference program
    ids; the stricter-verifier refusal, the legacy-bytes known-answer test, and every chain test, ignored ones included.

Opt-out. LAMBDA_VM_ZF_WHIR_GRIND_BITS=20 restores 20 bits and 112 queries, and the program ids of the branch
before this change.

What is in the branch

  • Per-table GPU recursion (STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009): per-table STARK proofs of each epoch on the device, LFM wraps and nodes, one root
    for the block.
  • WHIR recursion: WHIR base proofs, the WHIR-verifier wrap, the global wrap and the interior on the device.
    • The first full version proved the block in 148.9 s, already including the VRAM budget read from the driver
      (−8.3 s).
    • Evictable leaf-layer retention on the card saved −9.1 s. Fan-in 3 in the interior, plus the global child proved
      inside level 0's pool, saved −11.9 s. Together they took the block to 128.3 s.
  • Proof-format levers, taken from ZisK's recursion. One ZfFormat (prover/src/zf_format.rs) parses seven
    LAMBDA_VM_ZF_* knobs once and prints one ZF FORMAT: banner. The default is cap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6 whir_stack=27 whir_grind=query.
    • Merkle caps on every STARK and WHIR tree. Paths stop at a verifier-chosen height c ≤ 3, and the cap rides at the
      end of each tree's first path, so the proof structs are unchanged.
    • FRI folds by 2^d per committed layer, one challenge each, with a verifier-side DP schedule.
    • A six-variable first WHIR fold, schedule [6,4,4,4,4,3] at 25 variables: one round and three grinds fewer per
      chain.
    • The WHIR stack cap, 25 | 26 | 27, default 27: an epoch's WHIR base stacks 2–3 polynomials of 2^27 instead of
      8–11 of 2^25.
    • Grinding only before the queries (P2-W), whir_grind, default query: one grind and one nonce a round in
      the WHIR chains; see "Grinding only before the queries" above.
    • One-row openings with a committed FRI input (LAMBDA_VM_ZF_ONE_ROW=auto) are built on host, on the GPU and
      in-guest, but are off here: they cost +3.2 s on this pipeline. They are on in the STARK pipeline's PR (STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009).
    • Every knob keeps its off value, and ZfFormat::LEGACY stays pinned by a golden test. The RV64 recursion guest
      verifies only the legacy format.
  • Column-major LDE engine (crypto/math-cuda/src/lde_cm.rs, kernels/ntt_cm.cu).
    • A device LDE used to take about 33 whole-matrix DRAM passes: spread, per-level NTT tiles, bit reversal, weights,
      zero fill, and a transpose before a row-major commit.
    • The engine computes the coset LDE of as many columns as fit in 60 % of L2 at a time, 4 to 8 butterfly levels per
      launch in registers and shared memory, so a 2^22 transform is three passes. The coset spread is fused into the
      first pass, and the output is column-major, so the commits lose their transpose.
    • Every field value, leaf and root is the legacy one.
    • The STARK main, preprocessed, auxiliary, composition and batch LDEs go through it, and so does the WHIR base
      commit's encoding. LDEs that keep a host copy (below 2^19 rows) stay on the old path.
    • LAMBDA_VM_LDE_LEGACY=1 sends every LDE back to the per-level pipeline.
  • The gap-fix batch, the fixes in the table above, merged in three rounds:
    • gap-fix/ntt (da2da9d93), gap-fix/wbatch-int (7e3eac501), gap-fix/harness (f81f0a80f),
      gap-fix/rec-int (c679a771b), gap-fix/stack-int (8c2450ff6) and gap-fix/idle-a-int (d3c76d2ed), each
      a signed merge;
    • gap-fix/idle-b-int (bdb2d37b6) and gap-fix/hash-int (e041e9fb0), each a signed merge;
    • gap-fix/kern-int, one commit (d1dc45514).
    • Every fix keeps a named opt-out that reproduces the previous program set.
    • Only the three that change the recursion programs or the WHIR layout move program ids: BITWISE, the lean fold and
      the stack.
  • Grinding only before the queries (P2-W): 9cea599a3 (the nonce layout, default off), ce292de3e (the default)
    and b4506b719 (comments).
  • The argue's short, wide tables on the GPU (A1): 93a2b5643 (behind its knob), merged as c7228f310, and
    8930490e5 (the default).
  • The argue's challenge tables on the GPU (A2+A3): 34c17603b, merged as 7364d1292, and b9698b05d (the
    default).
  • Pure WHIR recursion: whir/full-recursion (4150afab6), merged as 3722e7376; 70cdb3719 (its pins under P2-W)
    and 6a6e26611 (the default).
  • Narrow sumcheck rounds on demand (N1′): 1177d5a13 (the census) and 965e13de2 (behind its knob), merged as
    06d2d48e8; 26adbf501 (the default) and a28ad36af (the W-LFM parity tests). The merge also carries two argue knobs
    whose A/Bs read MECHANISM-ONLY, LAMBDA_VM_ARGUE_LEAN_READS and LAMBDA_VM_ARGUE_LEAN_TAIL, both off.
  • The RPX MDS fix (d61a3c729) and deterministic whir_chain grind tests (d6648e653, merged as 5f15641b9).
  • The card permit after the host prep, and fan-in 4: 1e3c39d5e (a device-entry counter), 533a22926 (behind its
    knob), 78781f7cf (the default) and 4ab853c6c (the pure-WHIR tree's default fan-in 4).
  • Small blocks: 529589d9d (a tree of one wide level runs root option A as option B) and 61b025b7d (a spin guest and
    a test that proves 1 to 6 epochs to a verified root). A block of 2 to fan-in epochs used to stop at the root's
    child-count check; block 25368371's tree is unchanged.
  • Fan-in 5: e783f29d5 (the WHIR driver takes fan-in 5 from LFM_CENSUS_FAN_IN) and b682091a3 (the default).
  • The WHIR base's head ahead: d117ffedd (behind its knob) and eb20fe04f (the default). The GKR tree's
    refusals counted
    : fd3146a0b (the fan-in-5 margin quoted in MiB), 7650b53c7 (the counter), a28690b7f (its
    forced-refusal test) and 4593752a7 (the test's feature note). Merged as 33232d688.
  • The wide lead-in's early claim, landed and reverted: deb9726b3, reverted by 9e2728955 (whose tree equals
    33232d688's). Its A/B read −0.50 s (job 244), but at the landed head it measured no effect: 40.30 s with the
    claim and 40.30 s without (job 245). Level 1 gained ≈ 0.2 s and the base paid it back.
  • The WHIR leaf layers kept through a futile miss: 63ba2baf0 (behind its knob), b8148e4fe (its card test),
    bb7f5d8ee (the count on the retention line) and 73342bc66 (the default). LFM_WHIR_KEEP_FUTILE=0 opts out.
  • Whole WHIR trees kept, evictable: 3332c1bf3 (behind its knob), f891296de (its card byte-identity test),
    22b81fc63 (the served openings on the retention line), c68d37f16 (the default, and the comments that said a tree
    is never kept) and 7c8272701 (the retention card tests in both modes, and H4's memory guard restated as "nothing
    of a tree outside its promise"). LFM_WHIR_WHOLE_TREES=0 opts out.
  • A kept tree's promise given back by its last handle: 45fb0774f (the evictor passes over a tree an opening
    reads; its card test and two mutations).
  • The argue's zerocheck, stage 1: 53d52af9c (the host reference multilinear::fused, parity on every table, a
    test-only capture hook), a14f35b8a (the card rounds, behind their knobs), 1a07ce50d and 6b80511f8 (the card
    tests run alone, each table at a height the card uploads), 6ba799072 (the tables named on their log lines),
    4e40b5d09 (a table without roots launches no grid pass: the crash the first timing found), 5cbbb683f (both
    defaults on, their banners and the split's fused row) and f3d359998 (the canonical-bytes test and the fused fault
    in stark; the faults per thread). LAMBDA_VM_ARGUE_FUSED=0 and LAMBDA_VM_ARGUE_INT_NODES=0 opt out.
  • The argue's GKR layers with Gruen's split (M1-1): 1946609c3 (the kernels, GruenLayer, gkr_gruen with its
    host reference, the cross-check, the card and tall-table tests, behind the knob) and 7d82a320f (the default, pinned
    by a unit test). LAMBDA_VM_ARGUE_GKR_GRUEN=0 opts out.
  • The argue's split and census: 6623da0cb (ARGUE REST per epoch, ARGUE AIR per table, under
    LAMBDA_VM_BASE_SPLIT), 35265830f and fff43adf3 (ARGUE TREE, and LAMBDA_VM_ARGUE_TREE_SYNC, a diagnostic).
  • The argue's GKR input from the base columns, and no lift (M1-2): 87ed8dbcd (the kernel, the plan, the
    columns' tree, its card and tall-table tests, behind the knob), ba784bd0b (no lift: the strided fused kernels,
    ColumnFactors, the late lift, behind its knob) and d108cabb5 (both defaults, pinned by unit tests).
    LAMBDA_VM_ARGUE_GKR_INPUT=0 and LAMBDA_VM_ARGUE_NO_LIFT=0 opt out.
  • The level-1 latch: 395829a79 (the latch, the deferral and the opener in device_permit, behind
    LFM_TREE_GLOBAL_AFTER_LAST), 3d20815a2 (each card hold's proof named on its trace line), efdb528d7 (the
    lever's mode: no global in the pool, too few workers, small blocks) and 88b0d3196 (the default, pinned by a unit
    test). LFM_TREE_GLOBAL_AFTER_LAST=0 opts out.
  • main: perf(alloc): compile jemalloc's never-purge policy into the binary #996 (jemalloc never-purge compiled into the CLI).

Soundness

Query counts and blowup are unchanged.

The format levers

  • Caps. The root is still the commitment. The cap is hashed to the root once per tree, and each path must reach the
    cap node the query index selects. Path lengths are checked exactly, including at c = 0.
  • FRI folds by 2^d. This is Haböck (eprint 2022/1216) Protocol 1 / Theorem 2 with reduction factors 2^d. Only Σaᵢ
    changes, in a term that stays more than 50 bits below the dominant one.
  • One-row openings. This is batched FRI with the DEEP codeword committed before the first fold challenge, the
    layout Plonky3 uses. The query index is uniform over the whole domain.
  • WHIR first fold. Only the grouping of variables into rounds changes. Every error term is invariant or shrinks with
    fewer rounds, and queries stay 112 per round.

The gap fixes

  • The WHIR stack cap is a verifier-side constant. The prover, the host verifier and the recursion's WHIR emitters
    all take the layout from global_layout(shapes, cap), never from a proof.
    • A proof stacked under one cap is refused under another, and so is a prepared commitment.
    • The query count is now charged for the tallest stacked polynomial of the proof's layouts, not the widest single
      table. It stays 112 at every production shape.
    • Proven bits, per phase (BCHKS25 Thm 4.2 in the Johnson regime, the calculator security/zisk_calc.py): the WHIR
      chain minimum is 130.393 bits at 27, against 130.926 at 25. With the per-table STARK recursion
      (LAMBDA_VM_LFM_PROVER=stark) the pipeline minimum is 128.946 bits, set by the query phase of every LFM proof; with
      the default pure WHIR recursion it is 130.393 bits (see "Pure WHIR recursion").
  • The per-round WHIR fold proof-of-work earns no credit as placed. It is ground before each round's first sumcheck
    message, so a cheating prover can re-draw α₁ by varying h₁ without grinding again. The bits above are the unground
    ones. P2-W drops the folding and out-of-domain grinds (see "Grinding only before the queries"); K4 changes only how
    the card searches for the nonce.
  • BITWISE only where used. BITWISE only receives lookups, with prover-chosen multiplicities. In a program with no
    sender, its honest multiplicities are all zero and the table constrains nothing.
    • The dangerous direction, a sender without its receiver, cannot be built. The mask is derived from every
      instantiated chip's interactions, stored in the artifacts and folded into program_id, and the verifier re-checks
      it against the mask it was handed. No proof supplies it.
    • Tests refuse a forged mask and a drop under a byte-lookup family.
    • LAMBDA_VM_LFM_KEEP_BITWISE=1 reproduces the legacy registry digests.
  • The lean coset fold changes verifier arithmetic inside the emitted wrap program, not the proof format. Both
    emissions compute the same field value on accepted and on tampered chains (tests). The WHIR wrap program ids move.
  • The LFM_HASH split (off here) is program shape: committed per chunk, bound into program_id and never read from a
    proof. Tests refuse a forged tail root, the single-table door and a wrong chunk root.
  • Everything else is byte-identical:
    • the memory kernels: raw-identical to the per-level kernels, by a host known-answer test and device parity;
    • the room: ledger only;
    • the engine's WHIR encoding: the same codeword, tree and proofs;
    • the base prep and DECODE reuse: the same derivations, on another thread or reused;
    • the grid split: the same launches up to stack 26;
    • the staging (I6): the same bytes through another pinned buffer, by round trips across chunk boundaries and commit
      parity through either staging, on the card;
    • the lead-in (I7): the same prologue, built earlier; a lead-in prologue equals the one built from the bundle (test);
    • the RPX kernels (K3, K4, K5): the same digests, roots and smallest grind nonce;
    • the DEEP/OOD inversion (K6): the same field elements, by parity of each part against the legacy path and a CPU
      reference, and the fault suite under both settings;
    • the WHIR retention (keep futile, whole trees, the promise by its last handle): whether an opening copies a kept
      leaf layer, reads a kept tree or hashes the leaves again; the same tree, root and paths, by card tests that compare
      against a fresh codeword both ways, and the program ids of every A/B arm and gate tree;
    • the argue's zerocheck (stage 1): the same round messages, by construction (each technique an exact polynomial
      identity), by the cross-check arm on the block (390 of 390 tables' messages equal to today's), by the tall tables'
      canonical proof bytes on the card, and by the program ids of every A/B arm and gate tree;
    • the argue's GKR layers (M1-1): the same round messages and factors, by construction, by the cross-check arm on the
      block (3,381 of 3,382 layers compared equal, one not shadowed for room), by the tall tables' canonical bytes on the
      card, and by the program ids of every A/B arm and gate tree;
    • the argue's GKR input from the base columns and no lift (M1-2): the same input-layer cells and fused rounds, by
      the plan's host test against the lifted layer, the card tree tests, the tall tables' canonical bytes on the card
      (with and without the lift), and the program ids of every A/B arm and gate tree;
    • the level-1 latch: one card request moved in time, nothing computed differently; the program ids of every A/B arm
      and gate tree.
  • K5 is a different implementation of the same permutation: 32-bit limb multiplies with carry chains instead of
    the 64-bit multiply. Its bytes were shown equal three ways:
    • a host known-answer test runs every permutation variant against the RPX oracle (raw states and chained probes,
      with a failing control). It also replays the cooperative kernels lane by lane (the half-warp Merkle kernels and
      the queue grind) against the shipped ones. CI runs it on every PR;
    • on the card, each switch's two paths agree byte for byte (nodes, nonces, raw permutation states), path against
      path and, where cheap, against the host oracle (rpx_device_paths);
    • in its own ABBAs, every setting proved the same program ids and census.

Fixed along the way

  • One-row verify. The verifier's Phase-A transcript replay absorbed the row-pair root of one-row preprocessed
    tables, which rejected honest proofs that publish values.
  • A WHIR commit past CUDA's grid limit. At stack 27 the first NTT tile asked for 65,536 blocks in y. The launch
    failed, the error was dropped, and every such commit fell back to the host; the base took 647 s. The grid now splits
    into z, and device commit and tree errors are logged and counted.
  • A dropped leaf layer returns its bytes to the room it grew. Before, a fold's layer stayed promised until its
    source codeword dropped.
  • Concurrent census panels no longer interleave in a log. Each panel is printed in one write.
  • The prove split's device-grind count now includes RPX grinds. Under RPX it read 0 on every table.
  • Device byte-parity tests now run on a card. S3/S2 vector proofs and LFM proofs are byte-identical between CPU and
    GPU.
  • Comments are self-contained. The format code's comments point at nothing outside the repository.

Gate and CI

M1-2 and the level-1 latch were gated together, in one run at this head (88b0d3196), FAST job 401: GATES GREEN, 18 of 18 steps (guest
267/267; math-cuda 279 passed, 16 ignored; RPX device parity 11; stark 423 passed, 6 ignored; crypto 164; the 12
landing lines below; the prover's lib suite 1,650 passed, 100 ignored). Its latch lines: the latch's 14 unit tests (the default and the opt-out, the mode and the deadlock
guard, the latch, the deferral, the opener on unwind, the holder names); the production tree at the defaults (the
latch's banner, exactly one CARD DEFER, the global's, inside its bound, the global's prove after L1N2's artifacts),
under LFM_TREE_GLOBAL_AFTER_LAST=0 (the off banner, no CARD DEFER) and with three workers for level 1's four tasks
(the guard's INACTIVE line, no CARD DEFER), each with #1010's 5 program ids; and the six small blocks (the fixture
tree's no-global INACTIVE line six times, no CARD DEFER, every root verified). The same latch lines ran green on
the latch alone at 3a9015138 (7d82a320f + the latch), FAST job 400. Its M1-2 lines: on the host, the knob pins,
the plan and the split lines (59); on the card,
--test argue_gkr_input (2) and --test argue_gkr_gruen (4), stark's multilinear_table module (the canonical-bytes
tests, the no-lift arm reading the columns), and the six fused card tests through the strided kernels; the production
tree at the defaults (≥ 200 trees from the columns, ≥ 200 tables with no lift), under LAMBDA_VM_ARGUE_NO_LIFT=0 and
under LAMBDA_VM_ARGUE_GKR_INPUT=0, each with #1010's 5 program ids. Before it, each half's device gates ran green in
its A/B job (FAST 333 and 335).

M1-1 was gated at 7d82a320f, on the FAST2 box (job 200): 15 steps, all green — guest artifacts 267 / 267,
math-cuda 279 / 0 / 16, RPX device parity 11, stark 422 / 0 / 6, crypto 164, the lib suite 1,640 / 0 / 100. Besides the
standard steps:

  • on the host, multilinear --lib gkr whir_split (48: the host reference, the default pinned, the split lines);
  • on the card, --test argue_gkr_gruen (4) and --test argue_lean_tail (3) under the new default,
    -p stark --features cuda,multilinear/cuda --lib multilinear_table (35, the canonical-bytes and fault tests among
    them) and -p multilinear --features cuda --lib (374);
  • the cross-check's comparison made inert, which must redden the fault test: it did, and the tree was clean after;
  • the production tree at the default (3,382 Gruen layers, the default's banner, WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s #1010's 5 program ids), under
    LAMBDA_VM_ARGUE_GKR_GRUEN=0 (0 Gruen layers on every epoch, the opt-out's banner, the same ids), and at the
    default with the cross-check (3,381 of 3,382 layers compared equal, the same ids), each root verified.

Stage 1 of the argue was gated at f3d359998, on the FAST2 box: 16 steps, all green (the lib suite
1,640 / 0 / 100). Besides the standard steps: the defaults pinned on the host; on the card, the fused rounds against
today's host rounds (42 runs), the twelve tables without roots under the production switches, test_keccak proved and
verified with and without the cross-check, and the integer-node rounds; both negative controls with their mutations
red; real traces under the cross-check, none parted; -p multilinear --features cuda --lib and -p stark --features cuda,multilinear/cuda --lib multilinear_table (the canonical-bytes test on the card among them); and the production
tree at the defaults (298 fused sessions, 947 openings served, #1010's 5 program ids) and under both opt-outs (no fused
session, the same ids).

The kept tree's promise by its last handle was gated at 45fb0774f, on the FAST2 box: 13 steps, all green (the lib
suite 1,634 / 0 / 96), with its card test, both mutations failing it, the retention files under
LFM_WHIR_WHOLE_TREES=0, and the production tree at the default (952 openings served, 0 passed over, #1010's 5
program ids).

Whole trees were gated at 7c8272701, on the FAST2 box: 14 steps, all green (the lib suite 1,634 / 0 / 96).
Besides the standard steps: the retention unit tests with the default pinned; on the card, a kept whole tree serving
its openings byte-equal to the leaf-layer reference, and the futile miss at the new default; the tree-cache file at the
default and under LFM_WHIR_WHOLE_TREES=0 (H4's group guard in both modes: pool share 0 MiB, the ledger exact); and
the production tree at the default (952 openings served, #1010's 5 program ids) and under the opt-out (0 served,
the same ids).

Keeping the leaf layers through a futile miss was gated at 73342bc66, on the FAST2 box: 12 steps, all green (the lib
suite 1,634 / 0 / 96), with the card test both ways and the production tree at the default and under
LFM_WHIR_KEEP_FUTILE=0, each with #1010's 5 program ids.

The head ahead and the refusal counter were gated at 33232d688, on the FAST2 box: 14 steps, all green
(the lib suite 1,634 / 0 / 96). Besides the standard steps:

  • the head's three tests on the device build, and every small block (1 to 6 epochs) with the head ahead;
  • the production tree at the default and under LAMBDA_VM_WHIR_HEAD_AHEAD=0, each with the fan-in-5 landing's 5
    program ids;
  • the refusal counter's call-site censuses, and a forced refusal on the card beside its control.

A supplementary gate ran the per-table argument's suite on the card, 5 steps, all green. An audit found that the
suite's earlier gate line (-p stark --features cuda) ran multilinear's host paths only: stark's cuda does not
turn on multilinear/cuda. The supplement names both features and requires every device arm's own output line.

Fan-in 5 was gated at b682091a3, on the FAST2 box: 15 steps, all green (the lib suite 1,631 / 0 / 96).
Besides the standard steps: the root, tree-shape and knob suites; 1 to 6 epochs proved to a verified root at the
default (fan-in 5), under LAMBDA_VM_LFM_WIDE=off and under stark; the fixture tree to a block artifact; and the
production tree four times, at the default (its 5 program ids), under LFM_CENSUS_FAN_IN=4 (4ab853c6c's 6), under
LFM_CENSUS_FAN_IN=3 (5f15641b9's 9) and under LAMBDA_VM_LFM_PROVER=stark (the STARK recursion's 24).

Small blocks was gated at 61b025b7d, on the FAST2 box: 12 steps, all green (the lib suite 1,631 / 0 / 96),
including 1 to 6 epochs proved to a verified root at the default, under LAMBDA_VM_LFM_WIDE=off and under stark, and
the production tree's 6 program ids unchanged.

The permit after the prep and fan-in 4 were gated at 4ab853c6c, on the FAST2 box: 14 steps, all green (the
lib suite 1,628 / 0 / 95). Besides the standard steps: the permit's byte gate and device-entry gate, the fixture tree at
the new defaults and under LFM_CARD_AFTER_PREP=0, and the production tree three times, at the defaults (its 6 program
ids), under LFM_CENSUS_FAN_IN=3 (5f15641b9's 9) and under LAMBDA_VM_LFM_PROVER=stark (the STARK recursion's 24).

N1′, the MDS fix and the grind tests were gated at 5f15641b9, on the FAST2 box: 21 steps, all green (the
lib suite 1,625 / 0 / 94): the standard six, N1′'s 13 (card parity on the five big batches, the whole argument's
identity and its negative control, the slot-budget pin, multilinear's lib at the default and under the opt-out, and the
byte gate for both hashes and under the opt-out) and the MDS fix's 2 (the RPX suites).

Pure WHIR was gated at 6a6e26611, on the FAST2 box: 14 steps, all green (the lib suite 1,622 / 0 / 92). These are the standard steps,
plus the WHIR recursion's prover, verifier, leg, wide-node, switch and lead-in suites on the card with their negative
tests, the fixture tree at the default and under the opt-out, and the opt-out's byte gate: the production tree under
LAMBDA_VM_LFM_PROVER=stark prints 8930490e5's 24 program ids.

A2+A3 was gated at b9698b05d on FAST2: 17 steps, all green.

A1 was gated at 8930490e5, on the FAST2 box (the second RTX 5090): 14 steps, all green. These are the
standard steps, plus A1's device tests, the whole argument's identity and its negative control, the multilinear suite at
the new default and under the opt-out, and the WHIR byte gate on both hashes and under the opt-out.

P2-W was gated at b4506b719 on the FAST box: 11 steps, all green. These are the standard steps, plus the
multilinear suite, the WHIR chain gates with their production-shape emissions, the WHIR byte gate on both hashes, and
the WHIR epoch verifier programs under the opt-out.

The batch was gated at d1dc45514 on the FAST box: 81 steps, every one at its exact pre-registered count.
The standard steps:

  • guest artifacts 266 / 266
  • math-cuda 268 / 0 / 16
  • RPX device parity 11
  • stark 397 / 0 / 6
  • crypto 163
  • the lib suite 1578 / 0 / 90

The 75 targeted lines cover:

  • the engine and its legacy opt-out, the grid limits at stacks 27 and 28, the error counters, and the WHIR kernels and
    the room on the card;
  • the recursion-shape and registry tests, the stack lever at 25, 26 and 27, and the base-prep and DECODE schedule
    tests;
  • the staging round trips and the lead-in suites;
  • the RPX host known-answer tests, each RPX switch's path parity, and the old paths;
  • the DEEP/OOD parity suites and the fault suite, under both settings.

For each switch of the last two rounds, a gate line runs each setting and reads the banner its process printed. The
previous candidate without K6 (41549ebad) passed its own 70-step gate. The cumulative ABBA in the first table ran
after the gate.

In CI at d1dc45514, these pass: lint, the host known-answer tests (including the RPX lane-by-lane replay), the prover
test build, the stark cuda-feature tests, and the CLI and executor tests. The spec structure check fails on a key the
spec tooling does not know (spec/src/blake3.toml: constants). The prover shards were still running when this was
written.

Open decisions

  1. Merging. This PR and STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009 share the code up to P2-W (b4506b719). Since then this PR added the WHIR-side
    fixes (A1, A2+A3, pure WHIR, N1′, the permit after the prep, small blocks, fan-in 5) and STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009 the STARK-side ones
    (R1b, F-SIDLE, NICE v2); both carry the MDS fix. The shared defaults differ in the recursion prover (WHIR here, STARK
    in STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009), one_row (off here, auto in STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009) and the LFM_HASH split (off here, on in STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009). A per-pipeline default
    would let one PR carry both.
  2. Reporting configuration (I1). The record launchers export LAMBDA_VM_MEMPOOL_RELEASE_MB=0, which releases the
    device memory pool; the code's default retains it. Retaining measured −4.50 s on this pipeline before the engine, but
    only −0.45 s at the current head (inside noise), so this PR keeps reporting with the release. The STARK PR (STARK recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 60.18 s #1009)
    measured −2.00 s and reports with the retain.
  3. Stack 27 on a heavier block. On this block the heaviest epochs' argument sits within 2 GiB of the device ledger's
    budget at 27. A block that adds one polynomial to such an epoch moves that argument's reservation to the host:
    counted, proof unchanged, slower. The device peak before no lift was 29,266 MiB (the highest of an A/B's eight
    arms) of the card's 32,607 MiB. Worth a run on a heavier block before relying on 27 there.
    With whole trees kept, the base's heaviest argues run at the budget, and stage 1's zerocheck adds 523 MiB there: the
    argue takes kept trees back (10 a run, where 2 before) and pays their rebuilds. Everything kept is evictable, so a
    heavier block pays the old rebuilds first and falls back only where it fell back before; the same run would check
    it. M1-1 reserves nothing new and drops the layer-sized eq table its sessions allocated; its A/B left the argue's
    reserved peak unchanged (25,660 MiB) with 12.2 kept trees evicted a run against 10. M1-2's no lift then frees the
    lift's room: the argue's reserved peak is 24,153 MiB, below the budget, and 2.2 kept trees are evicted a run.
  4. Batched WHIR openings (not built). Their batch cap must be re-derived from the unground fold bits: at 27, K ≤ 5
    keeps 128 bits.
  5. Protocol changes. W3 (WHIR query carry-over) and W4 (the WHIR paper's rate schedule) are analysed, not built.
  6. An LFM lookup chip would let larger caps pay.
  7. RV64 proof bytes are not reproducible across processes, because six table builders order rows by HashMap
    iteration.

…ed FriFoldLayout

New `fri::schedule`: the integer dynamic program of FRI.md §2.1 that picks
the fold exponent of each committed FRI layer from public shape constants
only (first committed size, terminal size, query count, the cap-height
function as a parameter, DMAX = 6). Cost is kept in units of 1/Q so the
format function is u64-only, with the (cost, trees) lexicographic tie rule
(smallest d first). Also the FriMode / OneRowMode enums and FriFormat, the
verifier-side constants a layout is built from.

`FriFoldLayout` gains `schedule` (per committed layer) and `one_row`;
`num_committed = schedule.len()`. `FriFoldLayout::new` is now
`for_format(.., FriFormat::LEGACY)`, i.e. the all-ones schedule through the
general constructor, and produces the same total_folds / num_committed /
terminal_len / effective_k as before. The struct is no longer Copy (it owns
a Vec); no call site copied it. No caller uses a non-legacy format yet, so
no behaviour changes; ProofOptions and every serialized type are untouched.

Tests (fri_schedule_tests): the FRI.md §2.2 table pinned for T = 9 and 10
under three cap functions (off, the design model's, CAP.md §2 Auto,
implemented locally until the cap primitive lands); brute-force optimality
of the DP for b0 <= 16; legacy_layout_equals_old_layout against a verbatim
copy of the old constructor over B <= 30, blowup_log 1..4, k 0..10.
…fold word

WhirFolds becomes { Uniform, First(FirstFold) }: the first round folds k0
variables (all of them when the chain has fewer) and every later round is
today's walk, log_folding with the remainder last. A config serves chains of
every height, so a per-round list would have to say what a shorter chain
does with it; the lever design/WHIR.md measured is the first fold alone.
The C2 skeleton's Dp and List variants are removed (RULINGS 15: no DP, and
the knob is uniform4 | first5 | first6). FirstFold holds 1..=MAX_FOLD (6),
the widest fold the stack is tested at; nothing else is constructible.

- schedule(): Uniform runs today's body verbatim (tested against a copy of
  it for n <= 40, k 1..=6).
- with_security_folds(): Q is charged the worst round count of any chain of
  <= tallest variables under the schedule. Uniform gives exactly today's
  config (tested on the grid); first5/first6 never add a round at any height,
  so Q never rises, and it is 112 at 25 (6 rounds instead of 7).
- fold_word(): the statement word. Uniform = log_folding (4u64 at the
  default, today's bytes); First(k0) = 1<<63 | log_folding<<52 | 1<<48 | k0
  (design/WHIR.md's prefix encoding, one entry). Not absorbed yet.
- zf_format: LAMBDA_VM_ZF_WHIR_FOLDS accepts uniform4 | first5 | first6 and
  refuses dp, lists and other first<k>. WHIR_FOLDS_IMPLEMENTED stays false.

Host prove/verify at k0 = 5, 6 (n = 3..11), a tampered 64-wide base block
rejected, and a first6 proof refused under uniform4 (and back).
S5: CapPolicy::Auto is now RULINGS 1's table stated directly (3 from 20
openings, 2 from 4, else 0, clamped to the depth), with the thresholds as
named format constants. No arithmetic runs on the policy path, so no
verifier can disagree on an overflow. cap_gain stays (the FRI schedule DP
prices with the same weights) but is bounded: a height past MAX_CAP_HEIGHT
returns i128::MIN instead of shifting, and every term fits i128 for any
usize opening count, on 32-bit wasm too.

Pinned: the table is the cost-law argmax for every opening count up to
10^6 and at the usize extremes. The depth-clamped table and a
depth-bounded argmax differ at exactly one point (4 openings, depth 1:
table 1, argmax 0); a test pins that single difference.

M1: two fixtures that only one check rejects, on the real keccak backend:
- the real internal node above leaf 0 / leaf 2^D-1 presented as a leaf
  hash with a path one sibling short: the length-agnostic fold accepts it,
  only siblings.len() == D - c rejects it (every c < D, c = 0 is C1b);
- an unreached cap node flipped with 3 queries at c = 3: every per-query
  check passes, only the cap-to-root check rejects it.
Checked by hand: deleting the length check or the verify_cap call in
from_owner makes the matching test fail.
…itments agree on it

The three host absorbs (monolithic, epoch, global) and the LFM emitter's
push_config write ChainConfig::fold_word() where they wrote log_folding.
At the default schedule that is log_folding itself, 4u64, so every default
statement, transcript KAT and byte gate keeps its bytes; under first5/first6
it is a tagged word, so the 245-byte statement keeps its length and moves in
those 8 bytes only. The schedule of every chain is f(word, num_vars) and the
heights are already bound, so the word binds every schedule, including the
heights (num_vars <= 4) where first6 and uniform4 give the same schedule and
the same Q and only the word tells two proofs apart.

agrees_with (DecodePrepared, GenesisPrepared, GlobalPrepared) now compares
(log_blowup, log_folding, folds): commit_stacked blocks tree 0 at the
schedule's first fold, so a first6 commitment has 64-wide leaves that a
uniform4 epoch would open as 16-wide ones. One helper, committed_under,
makes the comparison for all three.

Tests: a_first_fold_statement_moves_only_its_fold_word (length kept, only
the fold word moves, the machine draws the host's challenge under first5 and
first6); the_fold_word_alone_separates_two_schedules_that_agree (mutation
gate: two configs equal in every field and schedule but the policy draw
different challenges at all three host sites; with fold_word() forced to
log_folding it and the test above FAIL, checked by hand);
a_decode_commitment_refuses_another_fold_schedule.
chain_config now builds through chain_config_under(format, shapes), which
calls ChainConfig::with_security_folds with the format's fold schedule, so
Q is charged the schedule's own worst round count rather than the uniform
one with the format stamped on afterwards. At the default this is exactly
the previous config (tested: chain_config_under(DEFAULT) == chain_config);
under first5/first6 Q stays 112 at the block's tallest stack (25), with 6
rounds instead of 7. decode_prepared_config and the five continuation call
sites go through chain_config and inherit the schedule.

ZfFormat::global() prints a second line under the banner, on every
setting: "ZF WHIR SCHEDULES: whir_folds=… q=… n=20:[…] … n=25:[…]", the
schedules the base chains run at the production heights, so a log states
the rounds it proved and not only the knob's name.

WHIR_FOLDS_IMPLEMENTED stays false until the GPU and in-guest gates land.
…ut, schedule override

RULINGS 13 / REVIEW-FRI F2: the fold-schedule DP now minimises the same
cost-law objective as the cap policy, per query per committed layer:
leaf blocks and walk levels priced with the cap policy's AUTO_WEIGHTS
(compress, select), minus the tree's cap gain; plus the slot mux
(2^d - 1 selects), the group fold (2^d - 1 binary folds at 5 XALU rows,
edsl::fri_fold) and the twiddle chain (d BALU muls). XALU and BALU rows
are priced from the node cost law at their committed widths (18 and 10
cells: 522 and 477 ns). FRI_COST_WEIGHTS is a format constant, pinned.
The generic DP (fri_schedule_by) keeps the design model's permutation
objective as a second instance, still pinned against the FRI.md 2.2
table, so the DP machinery stays checked against an independent model.

U1 is re-pinned from the Rust DP (T = 9 and 10, B = 6..24, cap Off and
Auto, S3 and S2 chains); at T = 9 under Auto it matches REVIEW-FRI F2's
independent cost-law column at every B it lists. U2 brute-forces the new
objective (4 cap policies x 3 query counts x 4 dmax, b0 <= 16).

RULINGS 18: FriMode / OneRowMode now come from stark::proof::options
(the local enums are gone); the cap input is a CapPolicy, and the test-
local Auto cap is CapPolicy::Auto.height.

FriFoldLayout::for_options builds the layout from ProofOptions (the
format is a verifier-side constant) and records the encoding: legacy
(pair leaves, one sibling per layer) exactly for fri=pair with row-pair
openings, decided by the format, not the schedule's values. A one-row
mode other than Off is an error (S2 is not built), never a silent
row-pair proof.

REVIEW-FRI F1.3: ProofFormat.fri_schedule_override, a test hook that
replaces the DP's schedule under fri=dp so round trips can use schedules
the DP never picks. No knob sets it (ZfFormat leaves it None); a schedule
that does not cover the table's folds is an error; is_default() requires
it None. ProofFormat is not serialized (skipped by serde and rkyv), so no
pinned byte moves.

No prover or verifier path uses the new layout yet; defaults unchanged.
W2's first6 schedule folds 6 variables in round 0, so tree 0's leaves are
64 base felts and the first fold runs six levels in one residency. GPU
parity covered k <= 5.

- whir_commit every_shape (both hashes): parity at (14, 2, 6), (12, 2, 6)
  and (7, 1, 6) (four leaves). Root, codeword, and openings against the
  host pipeline, through commit_codeword_to_host, which has no host
  fallback.
- whir_fold: the_device_folds_six_levels_as_the_host_does, six levels on
  the base codeword at 2^16, 2^14 and 2^8, against fold_codeword_k_on_host.
  It calls math_cuda::whir::fold_codeword_base directly, because
  whir::fold_codeword_k falls back to the host silently (size threshold,
  kill switch) and a comparison through it can be the host against itself.

Box only: the laptop has no CUDA (clippy with stub cubins is green).
…ifier (W1, C6)

Every WHIR commitment tree can now be opened under a Merkle cap: each
authentication path stops c levels below the root, and the tree's cap
(its 2^c nodes at that height) rides once, at the end of the tree's
first opening in proof order (the owner-path encoding, design/CAP.md
section 3). No struct changes, nothing new absorbed: the root is still the
commitment. At the default (CapPolicy::Off) every height is 0 and the
proof bytes are today's.

- ChainConfig::tree_caps: one height per tree from config.format.cap,
  CapPolicy::height(openings, depth) with depth = D_t - k_t and openings Q
  for tree 0, 2Q for every later tree (the last included). Shared by the
  prover, the host verifier and (next commit) the LFM ChainShape.
- CodewordCommitment::open_many_capped(indices, c, owner): paths cut to
  depth - c, the owner's first path carrying the cap. Host trees read
  MerkleTree::cap; device codewords read the cap in the SAME with_tree
  rebuild as the paths (DeviceCodeword::paths_and_cap, math-cuda + the
  multilinear gpu.rs wrapper), so the cap costs no extra tree build and
  retention/eviction are untouched. open_many is open_many_capped(.., 0, _).
- whir_round::prove takes RoundCaps; round 0 owns tree 0, every round owns
  its successor. final_openings likewise (a one-round chain's tree 0 is
  owned by the final openings).
- Verifier: TreeCheck { Owner, Checked }. A tree is authenticated ONCE,
  by CappedRoot::from_owner on its owner opening; round t opens tree t
  against the check round t-1 returned and never re-reads a cap (a cap on
  round t's first current opening fails its exact length). The checks are
  built after the opening-count guards, from .first(), so a short proof is
  refused, never a panic (REVIEW-CAP M2).
- Default-path hardening, the WHIR analogue of C1b (RULINGS 3/16):
  whir_commit::verify_opening now takes the tree depth and requires an
  exact-length path (CappedRoot::uncapped). Honest proofs are unaffected;
  only malformed proofs see a difference. New verify_opening_capped.
- New errors: CapRejected (verifier), CapEmbedFailed (prover).

Tests (laptop, multilinear --lib whir_*): capped chains round-trip at
Fixed(1..3) and Auto, Q 3 and 25, one-round, multi-round, remainder,
base and extension rounds, keccak and RPX, with every path length pinned
(depth - c, owner + 2^c); Off and Fixed(0) give identical rkyv bytes;
transcript invariance Off vs Fixed(3)/Auto (REVIEW-CAP S2); tamper arm
(tree-0 cap, tree-t cap in rounds[t-1].next[0], a second cap on round t's
current[0], owner path +-1, non-owner path +-1, cap moved to query 1, a
sibling, a proof read under another policy); M1(b) an unreached cap node
that every per-query check accepts, refused as CapRejected only by the
cap-to-root check (both trees of a round); M1(a) a keccak leaf forged
from an internal node (8 base values = 64 bytes = a parent input) that
the raw fold accepts, refused by the exact length alone; M2 an empty
capped round refused without a panic; tree_caps pinned at production
(Auto [3,3,3,3,3,3,2]).
No emitter change: ChainShape builds from config.schedule, so the closed
forms, the arena layout, the query phase and the slot mux follow the
schedule. What was missing is gates at k = 5 and 6.

- whir_fold_tests SHAPES gain (12, 5, 7) and (13, 6, 7): blocks of 32 and
  64, closed form, interned constants by value, and the fold against the
  host over extension and base blocks.
- whir_chain_tests: KNOB_COST_SHAPES (S = 9 first6 [6,3], S = 11 first5
  [5,4,2] at grind 0 and 8; S = 6 [6] and S = 7 [6,1] under first6) join
  the schedule gate (the emitter's hash schedule is the host transcript's)
  and the closed-form gate, through a cost_configs() list that keeps
  COST_SHAPES at the default schedule. A first-fold chain executes on a
  proof the host accepts; the tamper arm refuses the last value of a 64-wide
  base block, a round-0 sibling and the successor block, each rejected by
  the host too.
- Knob-on production pins at S = 25, Q = 112, grind 20, derived by hand
  (parents, leaf blocks) and equal to design/WHIR.md's independent model:
  first6 19,600 opening / 19,877 chain permutations / 201,318 rows; first5
  20,832 / 21,109 / 189,028. The ignored F1 at the production shape emits
  both programs and matches (run on the laptop: 0.08 s, 153 MB).
- PREPARED_LEG_ROWS stays fixed (RULINGS 15); a knob-on test asserts it
  still covers the 20-variable stack: 137,321 rows under first5, 155,889
  under first6, against 175,066.

Default pins unchanged (22,512 / 22,828 / 185,509 and the band test).
Every tree of a univariate STARK proof (main, precomputed, aux, composition
and each committed FRI layer) now honours ProofOptions.format.merkle_cap.

- merkle_caps.rs (new): StarkCaps, the one place the heights are computed
  from public shape (policy, query count, log2(lde), committed layer count;
  trace trees log2(lde)-1 deep, FRI layer i log2(lde)-i-2); TreeCheck, the
  verifier's per-tree check (built once, then used for every query);
  TableTreeChecks.
- Prover: a post-pass in round 4 after the openings. Per capped tree it
  reads the cap from the host tree and embeds it on the owner path (query
  0), cutting every path to D - c. Round 4 now returns a Result. A
  device-resident tree (root-only host tree) is a hard DevicePath error that
  names the tree until the device read lands (REVIEW-CAP S6); a host tree
  whose depth is not the format's is refused.
- Verifier: table_tree_checks builds every tree's check once, after the
  query-count and opening-width guards and with length-checked access only,
  so a malformed proof rejects and never panics (REVIEW-CAP M2). At c = 0 it
  reads no opening at all and is exactly the C1b exact-length check. Every
  opening (trace, precomputed, aux, composition, FRI layer) goes through its
  tree's check with its query position; query 0 of a capped tree uses the
  owner siblings split off once.

Nothing is absorbed, so the transcript is unchanged, and at the default
every height is 0: no path is cut, no cap is appended, the bytes are the
same. MERKLE_CAP_IMPLEMENTED stays false until the device arm (C4) is in.

Tests (tests::merkle_cap_tests, small AIRs, laptop):
- round trips at Fixed(1..=4) and Auto, 3/8/30 queries, blowup 2 and 4,
  owned and archived (rkyv, multi_verify_archived), with the path shapes
  pinned (owner D-c+2^c, others D-c, the cap hashes to the root);
- a preprocessed table and a RAP (aux) table capped, every cap node bound;
- Off == Fixed(0) == Auto-at-3-queries, byte for byte;
- REVIEW-CAP S2: Off vs Auto at grinding 0 give equal roots, OOD values,
  final coefficients, nonce and opened values; only paths differ, each the
  full path cut to D - c (+ the cap on the owner);
- tampers: every cap node of main/composition/first and last FRI layer,
  every node of a later query's path, the owner one node short/long, the
  cap on a non-owner, the cap moved to query 1, and a proof made under one
  policy verified under another (both directions);
- REVIEW-CAP M1 at the verifier level: an unreached cap node (3 queries,
  c = 3) that only the cap-to-root check rejects, and the real internal node
  above a queried leaf passed as a leaf hash, which the verifier's own
  TreeCheck refuses and the length-agnostic fold accepts. Deleting
  verify_cap or the length check from the primitive fails both (checked by
  hand);
- S6: a root-only tree with no device read is an Err naming the tree.
…1, C7)

The device side of W1 landed with the host commit (the Codeword::Device
arm of open_many_capped must compile): DeviceCodeword::paths_and_cap reads
the cap as the heap slice [2^c - 1, 2^(c+1) - 1) of the node buffer the
paths are gathered from, inside ONE with_tree rebuild. These are its box
gates; the laptop has no CUDA, so they only compile here.

- math-cuda/tests/whir_cap.rs: for k = 1..5 under keccak and RPX, every
  cap height up to min(depth, 6): the device paths equal the host tree's
  full paths, the height-0 cap is the root, and the device cap equals the
  cap the host owner encoding appends. In three leaf-layer regimes: served
  from the retained layer (0 extra leaf passes), rehashed at another
  blocking (1), and after the allocator's evictor reclaimed the layer.
  Each call is exactly one tree build (tree_builds + 1): the cap costs no
  extra rebuild.
- multilinear/tests/whir_cap_device.rs (cuda-gated): a 2^16-variable chain
  whose codeword stays on the card (asserted) proves the same rkyv bytes
  as the chain over a host-held codeword at Off, Auto and Fixed(5), both
  hashes, and the host verifier accepts it; tree 0's owner path length is
  pinned.
LAMBDA_VM_ZF_WHIR_FOLDS=first5 | first6 is now selectable: host chain,
statement word, agrees_with, the production config, the GPU parity cases
at k = 6 and the in-guest gates at k = 5 and 6 are in. The GPU parity
tests run on the box (no CUDA on the laptop); the knob-on block proofs are
the box request that follows.
REVIEW-FRI F1: nothing proved the default FRI format byte-identical. A
round trip cannot (a drifted prover accepts its own proofs), and proof
bytes are not reproducible under grinding (parallel nonce search). These
goldens prove at grinding_factor = 0, where the bytes ARE reproducible
(checked: two runs, identical), and pin, per case, the digest of the
proof's rkyv bytes plus separately its FRI layer roots, terminal
coefficients, FRI decommitments and trace/composition openings, so a
failure names the field that drifted.

Generated before any S3 prover code, on the schedule-DP commits (which
change no prover path):
- stark::tests::zf_golden_tests (SHA3-256): Keccak and Blake3;
  SimpleAddition (E = F) and LogReadOnlyRAP (E = F^3, aux); blowup 2
  and 4; total_folds 0, 1, 2, 3, 4, 6; one CPU/ADD/MUL multi_prove bus
  proof.
- prover tests::zf_rpx_golden_tests (SHA-256): the same AIRs under the
  production RPX pin (RpxStarkHash), which the stark crate cannot name.

Shown able to fail: swapping the pair order of the FRI layer leaves in
the CPU prover turns default_format_goldens_are_byte_identical red.

sha3 becomes a stark dev-dependency (the version crypto already links).
…ippy)

assertions_on_constants: WHIR_FOLDS_IMPLEMENTED is a const, so the check is a const block. make fmt and make lint green.
The R4 cap post-pass now reads a device-resident tree's cap instead of
refusing it: math_cuda::merkle::read_cap_dev is one D2H of the heap slice
[(2^c-1)*32, (2^{c+1}-1)*32) (the device heap has the host layout, so these
are the nodes MerkleTree::cap returns), and gpu_lde::read_cap_dev wraps it
with shape checks that fail closed with a message, never a panic. The
device arms: main and aux (gpu_main/gpu_aux trees, the table's bound
stream), composition (gpu_composition_tree) and each FRI layer (gpu_tree, a
fresh backend stream as the device FRI query phase uses). The precomputed
tree is always a full host tree. No kernel, no commit-phase change: paths
are still gathered in full and cut on the host (the merkle_gather parity is
untouched). New counter gpu_cap_read_calls.

MERKLE_CAP_IMPLEMENTED is now true (C3 + C4 are both in), so
LAMBDA_VM_ZF_CAP no longer aborts. The in-guest LFM verifier (C5) does not
verify caps yet: a recursion run that wraps a capped proof fails there, so
the knob is for STARK-level tests until C5.

Tests (box only; the laptop has no CUDA, cuda clippy is the laptop gate):
- math-cuda tests/merkle_cap.rs: keccak trees 2^1..2^8, 2^12, 2^18, 2^22
  leaves, every c <= min(D, 6): the device read equals the host cap and the
  heap slice, c = 0 the root; RPX trees equal the device's own heap slice;
  a kept composition tree (GpuMerkleTree) serves its cap and root;
- stark tests::merkle_cap_tests::device_trees_serve_their_caps (ignored,
  cuda): a 2^14-row cubic LogUp table proved at Auto/30 queries takes caps
  off the device (counter moves), verifies owned and archived, and matches
  an Off proof of the same witness with every path cut to D - c;
- zf_format::the_merkle_cap_knob_is_selectable.
prover/tests/merkle_cap_vm.rs proves an ELF through
prove_with_options_and_inputs with the options the process format names
(ZfFormat::from_env, so LAMBDA_VM_ZF_CAP), verifies it under the same
options, and checks the default-format verifier refuses it (Ok(false) or
Err, never a panic or an accept). Every production table is capped:
preprocessed precomputed + main trees, LogUp aux trees, composition trees,
FRI layers. CPU fixture all_instructions_64; under cuda fib_iterative_1M,
whose tables commit on the device, and the caps must come off the resident
trees (gpu_cap_read_calls moves).

Knob-on only: #[ignore], and it refuses to run with the cap off. Box only
(it proves a real trace).
… cost model (W1, C8)

The level-0 WHIR wrap now verifies capped chains (design/CAP.md 6.2).
Everything is derived from ChainShape.caps = ChainConfig::tree_caps, the
same heights the host prover and verifier use; at the default every
height is 0 and the emitted program, the arena and every pin are today's
(no new arena, no new word, the root path instruction for instruction).

- whir_open: CapCells, whose only constructor authenticate() hashes the
  hinted cap to its root (2^c - 1 compressions) and asserts it equals the
  tree's root lanes, once per tree. TreeAuth { Root, Cap }: every opening
  goes through TreeAuth::verify_opening with the WHOLE index; it walks the
  low bits and a private mux (2^c - 1 Selects, pairs (2t, 2t+1), low bit
  first) consumes exactly the top c, then compares two variable cells. So
  the cap the mux reads is the cap the root check read (REVIEW-CAP (e)),
  and no caller splits the index for the mux ((d), S1 in its WHIR form).
  Closed forms: verify_opening_{rows,perms}_capped, cap_check_{rows,perms}.
- whir_chain: ChainShape.caps, current_path (the sibling count; current_depth
  stays the index-bit count, the two meanings the map flagged). Tree 0's
  cap is authenticated at the top of emit_verify_weighted, each successor's
  where its root is unpacked, and carried to the next round with it.
  Arena: tree 0's 2^c words right after round 0's nonces, tree r+1's right
  after round r's successor root and ood value, paths depth - c
  (round_words, RoundStorage::hint, push_round_words split the owner path).
  Cost model: chain_opening_perms carries depth - c per opening plus
  chain_cap_perms; chain_query_rows the capped opening rows; chain_fixed_rows
  the per-tree cap checks. Hints stay arena words (the chain's plumbing).

Pins that move only with the knob on (all default pins unchanged):
production chain S=25 k=4 Q=112 grind=20 at Auto, caps [3,3,3,3,3,3,2]:
opening perms 22,512 -> 18,413 (-4,144 + 45), perms 22,828 -> 18,729,
shape rows 184,673 -> 187,245, rows 185,509 -> 188,081 (hand-derived in the
test doc, then run). Emitted at the production shape (ignored, laptop-safe):
188,081 rows / 18,729 perms == the forms; 37,968 Select (+5,152 a chain).
PREPARED_LEG_ROWS stays fixed (RULINGS 4): under Auto it is within 2% of
the 24-variable chain and still covers the 20-variable stack (tested).

Tests (laptop): capped chains execute on host-accepted proofs at Fixed(1),
Fixed(2), Auto, Q 3 and 25, one and three rounds; emitted rows and perms ==
the forms at Fixed(2), Fixed(3), Auto; the host transcript schedule is
unchanged under the cap; tamper: an UNREACHED tree-0 cap node (positions
from the host's own draws: only the cap-to-root check can refuse it), a
reached one, and a successor's cap node, each rejected by the host and with
no execution; the cap mux selects every index (all 64 leaves of a depth-6
tree, c = 1..3) and refuses the right leaf claimed in another subtree; an
unreached tampered cap word cannot execute; capped opening and cap check
closed forms at every height of a depth-6 tree.
…able

WHIR_CAP_IMPLEMENTED flips to true now that the cap is in the host prover
and verifier (C6), on the device (C7) and in the in-guest verifier and its
cost model (C8). ZfFormat no longer aborts on LAMBDA_VM_ZF_WHIR_CAP=auto or a
fixed height; the default (off) is unchanged. A zf_format test pins that the
knob is selectable.
…verifier (H2)

Under LAMBDA_VM_ZF_FRI=dp (ProofFormat.fri_mode = Dp) committed FRI layer
j folds by 2^{d_j}, d_j from the verifier-side schedule DP (FRI.md 1-3,
with REVIEW-FRI F5/F6 applied). The legacy format (fri = pair) runs
today's code, byte for byte: the H0 goldens are unchanged.

Prover (fri/mod.rs): commit_phase_with_layout. Per committed layer:
sample zeta, fold d_{j-1} times with zeta, zeta^2, ... (d_{-1} = 1: fold
0 is the binary fold of the DEEP pair; F6's fold-count fix), commit the
result, append the root; the final zeta folds d_last times into the
terminal. The fold is the unchanged binary fold. Group trees hash each
2^d-value group with H::Batched and build parents with H::Pair, as
today's layer trees (built with Pair, verified with Batched).
query_phase_with_layout opens the full group (the query's own value
included, FRI.md 3.4) and the path of leaf p >> d. Proof structs are
unchanged: the flat layers_evaluations_sym carries every layer's group
under a non-legacy format (its length a verifier constant).

Verifier: fri_termination_params builds the layout from the AIR's
options (never the proof); a format it cannot lay out is rejected. The
group checks live in fri::group::verify_query_groups: per layer the
group is hashed in full and authenticated at the exact depth, the slot
check group[p & (2^d - 1)] == v, and the group fold (d binary levels on
the fiber, x_g^-1 from the query point and the slot). The structural
check pins the value count per query before any loop.

The legacy/group encoding is decided by the format, not the schedule's
values (F5's per-table predicate reduces to the format until S2).

Device: every device FRI arm (DEEP-to-FRI on device, the device commit,
the device query gather) runs only for the legacy encoding; a dp table
takes the CPU FRI loop (DEEP may still run on the device, its values are
format-independent). One-row modes are refused (Err), not proved.
Round 4 now returns Result: an unsupported format is a ProvingError.

Tests (tests::fri_group_tests, prover tests::zf_rpx_golden_tests):
- U4 group_fold_equals_d_binary_folds (d = 1..6, every group and slot,
  and 2^d * sum zeta^i f_i from the polynomial);
- U5 group_leaf_is_a_coset (b <= 10);
- U6 round trips at dp: every fold count 0..9 at blowup 2 and 4; explicit
  schedules [1,3,3] [3,1,3] [2,1,2,2] [1]*7 [6,1] [1,6] [4,3]; ext3 with
  aux; a multi-table bus proof; Keccak, Blake3 and RPX; a non-covering
  override is a proving error;
- the format is a verifier constant (dp proof rejected under pair and
  vice versa);
- F1.2 generic_path_at_all_ones_equals_legacy (Keccak and RPX): same
  roots, terminal, openings, paths; each group is the legacy pair;
- T1-T3: every group value of a query (slot and non-slot), a path
  sibling, a root, values one short / long, a short path, a missing layer;
- M1 the slot check and M2 the group authentication are load-bearing:
  a p0 + c FRI forgery / a foreign root is ACCEPTED with the check
  switched off (test-only thread-local mutation) and rejected with it.
Shown able to fail: folding d_j instead of d_{j-1} (F6's bug) turns 8
S3 tests red while the goldens and the all-ones differential stay green.
FRI.md 10, "Vectors the host lane exports" (a)-(d), checked in under
crypto/stark/tests/vectors/zf_fri/ with a README (conventions: field and
limbs, bit-reversed coset layers, the binary and group folds, group-leaf
hashing, query/leaf/slot arithmetic, transcript, proof encoding):
(a) a_schedules.json: the DP's schedules and cost-law costs, T in
    {4, 9, 10}, Q in {3, 110}, cap off/auto, B = 6..24, S3 and S2 chains;
(b) b_group_folds.json: a SplitMix64 KAT codeword (2^7 ext3 values, the
    generator documented) folded d = 1..6 times; the generator asserts
    the verifier's group fold of every group reproduces the prover's;
(c) c_leaf_digests_{keccak,blake3,rpx}.json: the first group's leaf
    digest and the whole group-leaf layer root, d = 1..6;
(d) d_proof_{keccak,blake3,rpx}_{pair,dp,dp_3_1_3}.{rkyv,json}: a
    LogReadOnlyRAP proof (B = 12, blowup 4, k = 2, Q = 3, grinding 0) per
    format, with the layout, roots, every zeta, the terminal
    coefficients, and per query iota, the DEEP pair and per layer the
    position, leaf, slot, opened values and path length.

The generators live in stark::fri::vectors (test / test-utils only, so
the prover crate generates the RPX files with the same code). zeta,
iota and the DEEP values come from the host verifier itself, through a
test-only thread-local capture (stark::fri::capture). The tests
zf_fri_vectors::vectors_are_current (stark) and
tests::zf_rpx_vectors::rpx_vectors_are_current (prover) regenerate every
file in memory and require it byte-equal to the checked-in copy.

prover tests::zf_vm_dp_tests::a_vm_proof_round_trips_at_fri_dp: a real
multi-table VM proof (test_mul_8, the preprocessed tables included,
RPX, CPU FRI) proved and host-verified at fri = dp, rejected by the
default-format verifier and after a group value is tampered. It builds a
full VM trace, so it is a box test (lib suite), not run on the laptop.

The ZF FORMAT banner's fri field (fri=pair|dp) already exists (C2).
LAMBDA_VM_ZF_FRI=dp is now selectable: ZfFormat no longer aborts on it.

Implemented: the CPU prover (group-leaf layer commits, scheduled folds,
group openings) and the host verifier, owned and archived views. On a
cuda build every device FRI arm runs only for fri = pair; a dp table
takes the CPU FRI loop (DEEP may still run on the device).

Not implemented: device group-leaf FRI (I-FRI-D); the in-guest LFM
verifier of a dp proof (I-FRI-G: lfm::fri::FriShape still derives the
legacy layout, so emitting a wrap or node over a dp proof fails its
committed-layer assert); the RV64 recursion guest (default-only by
RULINGS 11, it refuses a non-default format). So a block run at
LAMBDA_VM_ZF_FRI=dp proves and host-verifies its STARK proofs but cannot
recurse over them yet. The zf_format lever test now pins that fri=dp is
not reported as unimplemented.
production_sites_prove_at_the_process_format proves and host-verifies a
small ext3 STARK under RPX with block_base_options() and
aggregation_wrap_options(), the two univariate production format sites,
and checks the encoding the process format implies. Without a knob it
pins today's legacy encoding; under LAMBDA_VM_ZF_FRI=dp (the box's
knob-on line) it asserts both sites stamp FriMode::Dp and the proofs
carry group layers. Checked on the laptop both ways (the dp run prints
"ZF FORMAT: ... fri=dp ...").
…path lengths in the STARK verifier, and the ZfFormat skeleton

C1 ab7208f adds the cap primitive and the auto cap-height policy (height at most 3, chosen by the
recursion cost law). C1b f82e42b makes the host STARK verifier require exact authentication-path
lengths. C2 77ea1ab adds ZfFormat, the one proof-format config, parsed once from LAMBDA_VM_ZF_*;
every lever is unimplemented at this commit, so any knob aborts and the default is byte-identical.

Gates on FAST at 77ea1ab (default format): prover lib 1466 passed / 0 failed / 82 ignored (base
1456 + 10), stark 325 / 0, crypto 158 / 0, multilinear 310 / 0, rpx device parity 11 / 11,
math-cuda 207 / 0, whir_transcript_configuration (hash-metrics) 3 / 0, byte and identity gates green.
Merges e6ea359 (lane I-CAP-S, zf/cap-stark
wave B): the cap review fixes on the primitive (R1), Merkle caps on the
host STARK path (C3) and off device-resident trees (C4), and the knob-on
VM proof test (T).

Conflicts resolved: none (clean merge onto zf/integration @ 59cd50a).
Merges d281c3b (lane I-CAP-W, zf/cap-whir): Merkle caps on WHIR
chains, host (C6), device parity tests (C7) and in-guest (C8), and
WHIR_CAP_IMPLEMENTED = true.

Conflicts resolved:
- prover/src/zf_format.rs (tests): I-CAP-S added
  the_merkle_cap_knob_is_selectable and I-CAP-W added
  the_whir_cap_is_implemented_and_selectable at the same spot. Both
  tests are kept verbatim; no other hunk conflicted.
Merges ac73346 (lane I-WHIR-F,
zf/whir-folds): WhirFolds::First, with_security_folds, the statement fold
word, committed_under in agrees_with, the production config under the
format, GPU k = 6 parity tests, in-guest k = 5/6 gates, and
WHIR_FOLDS_IMPLEMENTED = true.

Auto-merged without conflict: crypto/multilinear/src/whir_chain.rs
(with_security delegates to with_security_folds; tree_caps derives from
config.schedule(), so W1 caps follow the W2 schedule), math-cuda
whir_commit.rs / whir_fold.rs tests.

Conflicts resolved (tests only, no behaviour change):
- prover/src/zf_format.rs: the_whir_fold_lever_is_selectable added at
  the same spot as the two cap-selectable tests; all three kept verbatim.
- prover/src/lfm/whir_chain_tests.rs:
  - imports: the union (CapPolicy from I-CAP-W; ChainFormat, FirstFold,
    WhirFolds from I-WHIR-F).
  - both lanes introduced a helper named fixture_with with different
    signatures. I-WHIR-F's general fixture_with(&ChainConfig, num_vars)
    keeps the name; I-CAP-W's (num_vars, Q, grind, cap) helper is renamed
    fixture_capped and now builds config_with(Q, grind, cap) and calls
    the general one (the same config it built before). Its three call
    sites in the W1 section are renamed; nothing else changed.
  - the two appended sections (W1 cap tests, W2 first-fold tests) are
    both kept verbatim, W1 first.
Merges 6092c77 (lane I-FRI-H,
zf/fri-host rebased on 77ea1ab): the cost-law FRI schedule DP, the
fri_schedule_override test hook, S3 group FRI on the CPU prover and host
verifier, goldens and vectors, and FRI_MODE_IMPLEMENTED = true.

Auto-merged without conflict: crypto/stark/src/proof/options.rs
(ProofFormat now carries merkle_cap, fri_mode, one_row and
fri_schedule_override; MERKLE_CAP_IMPLEMENTED and FRI_MODE_IMPLEMENTED
both true), prover/src/zf_format.rs (proof_format sets the override to
None), crypto/stark/src/tests/mod.rs.

Conflicts resolved:
- crypto/stark/src/prover.rs, round 4:
  - the query phase: I-CAP-S made query_list mutable (the cap post-pass
    embeds caps into it); I-FRI-H switched it to
    query_phase_with_layout. Resolved as a mutable binding of
    query_phase_with_layout(&fri_layers, &iotas, &fri_layout).
  - I-CAP-S's embed_stark_caps / tree_cap helpers were one side of an
    add/nothing hunk; kept verbatim.
- crypto/stark/src/verifier.rs (textually clean, semantically broken,
  fixed without changing either lane's behaviour):
  - table_tree_checks (I-CAP-S) read
    fri_termination_params(..).num_committed, which I-FRI-H changed to
    return Option. Now `?`: a format the verifier cannot lay out makes
    table_tree_checks None, which rejects the proof, the same verdict
    step 3 gives it.
  - I-CAP-S removed step 3's `lde_log` binding (its legacy path takes
    the per-tree checks instead); I-FRI-H's group path still passes
    lde_log to verify_query_groups. The binding is restored, with
    I-FRI-H's comment, before the terminal codeword.

Not resolved here (reported to the lead, no code change): StarkCaps
derives FRI layer depths from the legacy layout; the group (fri=dp)
verifier path authenticates full paths and ignores FRI caps. So cap on
together with fri=dp fails closed (prover depth Err or verifier
rejection), a completeness gap, not a soundness one.
Under a fold schedule (fri=dp) a committed FRI layer is a group tree whose
depth is the fold layout's, not log2(lde) - j - 2. StarkCaps took the FRI
depths from the pair layout, and the group-path verifier authenticated every
layer with an uncapped CappedRoot, so LAMBDA_VM_ZF_CAP with
LAMBDA_VM_ZF_FRI=dp failed closed (M-MERGE-B note 2, REVIEW-FRI F9).

- StarkCaps::from_layout / for_options: FRI depths from FriFoldLayout;
  the prover (round-4 cap post-pass) and the verifier (table_tree_checks)
  both build from the layout they already hold. StarkCaps::new stays the
  pair-layout form for existing callers.
- fri::group::verify_query_groups authenticates layer j with the tree's
  TreeCheck (exact length D - c, query 0 the cap's owner, cap-to-root once
  per tree) instead of an uncapped root check.
- Default format unchanged: at cap=off every height is 0 and TreeCheck is
  the C1b exact-length check the group path already ran.

Tests (cap_fri_matrix_tests): {off, fixed(2), auto} x {pair, dp, dp [3,1,3]}
at Q=24 round-trips owned and archived with the layout-depth capped path
shape; every cap node of a capped group layer is bound; an unreached cap
node is rejected by the cap-to-root check alone; a proof made under one
(cap, fri) cell fails under the others.
The (d) proof vectors ran at Q = 3, where the auto cap policy caps nothing
(RULINGS 1: height 0 below 4 openings), so no vector exercised S1 with or
without S3. Two formats are added for Keccak, Blake3 and RPX at Q = 20:
cap_pair (cap=auto, fri=pair) and cap_dp (cap=auto, fri=dp), every tree
capped at height 3. Their JSON also records the verifier's StarkCaps
(trace/FRI depths and heights). The existing pair/dp/dp_3_1_3 files are
byte-identical (proof_options takes the query count; the new JSON lines are
written for capped formats only). cap_dp's FRI roots and zetas equal dp's:
the cap moves no transcript value.
verify_query_groups read the layer roots off the proof; it authenticates
with the per-tree checks since the previous commit, so the parameter was
unused (a -D warnings lint failure).
A committed table carries no name, so each ARGUE FUSED line gives its
signature - roots, bus terms, walk slots - beside its factor count, and a
declined table prints a line of its own under the cross-check or
LAMBDA_VM_ARGUE_FUSED_LOG. LAMBDA_VM_ARGUE_INT_NODES=1 prints one banner
when it is first read, so an arm's log shows it took effect.
The upload declines a table under 4096 cells (gpu::worth_the_device),
and the prover then argues it on the host, so a run at 2^8 of EQ's 12
factors never reached the card: FAST job 271's one red. Each table now
runs at 2^8 or the first height above it with 4096 cells.
A table without roots has a zero constraint part and d_C = 0, so its
grid {0,1}^2 is all corners; with the corners skipped - the production
setting, no cross-check - no grid point is left, and the launch sizing
divided by that empty row count. FAST job 273's B arm panicked there
("attempt to divide by zero", argue_fused.rs:101) at the first such
table; its X arm, whose cross-check computes the corners, proved and
verified with all 390 fused tables confirmed.

An empty point list now launches nothing (T is zero) and sizes no slot
file; U is still computed. A host test pins the sizing (it reproduced
the panic at the same line before this change). Two card tests close
the gap that let it through, both with the production switches: every
table without roots against today's host rounds, the corners skipped;
and test_keccak proved under the B arm's switches, with and without the
cross-check, and verified.
The FAST A/B at 9e27289 read EFFECTIVE twice (jobs 274 and 270, pooled
over 4 A and 4 B arms): base -2.80 s, the argue -2.94 s, the whole run
-2.75 s, every B arm's base below every A arm's, identities equal on all
eight arms, no fallback; the cross-check arm confirmed every one of 390
fused tables' messages against today's rounds.

LAMBDA_VM_ARGUE_FUSED and LAMBDA_VM_ARGUE_INT_NODES are now on unless set
to 0, and 0 is today's rounds exactly. Each prints a banner when first
read, and a unit test pins each default. A fused session counts as a
device sumcheck (gpu::sumcheck_calls, sumcheck_rounds), and the split's
ARGUE ZEROCHECK line ends with the fused sessions, the declined ones and
their device time, so an arm's log shows which rounds it ran.
…is refused

With the fused zerocheck on by default, the tall-table knob tests pin it
off: every other argue knob acts on today's rounds. Two tests on top: the
tall tables proved with the fused rounds (alone, and with the device
columns, tables and gathered reads) give today's canonical bytes table by
table and whole, the same transcript and next challenge, and verify - on
a device the fused arm shows its sessions; and with the fused rounds' bus
coefficient off by one the proof parts from today's at CPU and does not
verify.

The fused faults are per thread now: the fused rounds run by default, so a
fault armed for one test must not reach a proof another test runs beside
it, and a table's argument runs on the calling thread.
MauroToscano added a commit that referenced this pull request Sep 30, 2026
The process pool kept its two 3.22 GB slots for the life of the process. On
#1010's production tree the host peak is in level 1 (16.2 GiB against the
base's 9.6), where no epoch needs a slot, so kept slots would add all 6.4 GB
of them to the whole run's peak. D-TRACE-1B §3.4 sized the base's peak only.

SlotPool::release_free frees every block nobody holds; a later fill makes
them again, and until then a lease misses as not ready. The WHIR base calls it
on a helper as soon as the last epoch is proved, beside the cross-epoch proof,
and joins the helper before it returns (PINNED SLOTS: released … line).

Tests: releasing frees only unheld blocks, a held block survives and comes
back, the next fill remakes them (heap blocks, laptop); the one-slot pipeline
test now asserts the slot is back and released after the run.
@MauroToscano MauroToscano changed the title WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 40.20 s WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 35.95 s Sep 30, 2026
…AMBDA_VM_ARGUE_GKR_GRUEN

D-ARGUE S1-3 (D-BATCH M1-1). A device GKR layer's round sends
s(t) = E·eq1(u_j, t)·H(t), with H(t) the layer's h summed under
eq(u_{>j}, ·). The card now sums H(1) and H(2) only - every node by
additions, no eq factor and no program walk - and folds the previous
challenge on the way in; the host puts E·eq1 back, takes H(0) from the
claim (the card sums it where 1 - u_j has no inverse) and extrapolates
H(3). The weight is two small tables a layer (the tail's variables and
one level per card round), not a layer-sized eq table folded each round.
The rounds stop at a cube of LAMBDA_VM_ARGUE_GKR_GRUEN_TAIL (default 64)
and hand the host today's five factors there.

Every message is today's field element, so the transcript and the
canonical bytes do not move. Off by default: off is today's rounds.

- math-cuda: kernels gkr_eq_levels_ext3, gkr_round_gruen,
  gkr_gruen_finish; GruenLayer (tree layers and the rebuilt input
  layer); SumcheckSession::fold_first for the cross-check's shadow.
- multilinear: gkr_gruen (knobs, the host's half of a round, counters,
  a per-thread fault, a host reference of the device algorithm);
  DeviceTree::prove_layer_gruen; LAMBDA_VM_ARGUE_GKR_GRUEN_XCHECK walks
  today's program over the same folded halves each round and compares
  rounds and factors; the ARGUE GKR line ends with the Gruen counts.
- tests: the host reference equals today's rounds, challenges and
  factors at m 1..9 over every tail split, with H(0) direct or derived,
  and with coordinates of one; a wrong claim changes it. Device tests
  (tests/argue_gkr_gruen.rs) and stark's tall-table byte identity and
  fault test run on a card.
…GRUEN=0 the opt-out

FAST job 330 (4 + 4 arms on #1010 + Q1b): base -1.77 s, the argue
-2.08 s, the whole run -1.57 s, the GKR 4.24 -> 2.13 s, the same program
ids; its cross-check arm compared 3,381 real layers with the old rounds
and found every one equal. A unit test pins the default.
@MauroToscano MauroToscano changed the title WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 35.95 s WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 34.85 s Sep 30, 2026
…H S0)

Under LAMBDA_VM_BASE_SPLIT only, and byte-identical: the argue's "rest" -
what is neither a device GKR layer nor a zerocheck round, 3.19 s a block
known only by subtraction - is timed per region of a table's prove:
interactions, tree, output, gkr (its host prefix apart), setup, batch,
factor values, claim reduce, column evaluation, release. One
`ARGUE REST #k` line an epoch sums them against the argue's wall, and
one `ARGUE AIR #k t NAME` line a table gives the shape D-BATCH's plan is
sized from (n, columns, factors and how many read a shifted row,
interactions, k, degree, roots, widest bus message) with its tree, gkr
and core times.
Under LAMBDA_VM_BASE_SPLIT, an ARGUE TREE line an epoch splits the tree
region S0 measured at 1.80 s a block: the factors lifted, the input layer's
programs lowered and written, the levels folded, the output read. The card
runs the first three asynchronously, so LAMBDA_VM_ARGUE_TREE_SYNC=1 (a
diagnostic, never a wall measurement) makes each part wait for its own
kernels and charges it its card time. Byte-identical.
…d LAMBDA_VM_ARGUE_GKR_INPUT

D-BATCH M1-2, its first half. The tree region measured 1.80 s a block,
and its card time is mostly the input layer's writes (1.42 s of 2.39 s
under the sync split, FAST 332): two program launches an interaction,
each walking ext3 lifted factors through a slot file of its own. Under
the knob one launch writes every interaction's two sides straight from
the table's resident base columns - `constant + sum coeff * column` as
base x ext3 products, the same field values - from a plan built once a
table (`logup::input_plan`) and kept for the layer's rewrite. The tree,
its output and the GKR proof are the same; the factors are still lifted,
for the zerocheck. Off by default; off is today's path.

- math-cuda: kernel gkr_input_from_columns; gkr::InputPlan and
  gkr::input_from_columns.
- multilinear: the tree builder takes a writer (the lifted one or the
  columns' one), input_layer_tree_from_columns, logup::input_plan and
  resident_tree_from_columns, TraceData::resident_shared, a counter, and a
  test hook that hands every input layer back so its rewrite is reachable.
- tests: the plan's cells equal input_layer over the lifted factors
  (direct and shifted columns, padded or not; a mutated shift fails it); a
  public factor has no plan; on the card (tests/argue_gkr_input.rs) the
  columns' tree proves today's proof carried and handed back, and a wrong
  constant moves the output; stark's tall tables give today's canonical
  bytes with the knob alone and beside the production defaults.
…AMBDA_VM_ARGUE_NO_LIFT

D-BATCH M1-2, its second half. With the input layer written from the
columns (LAMBDA_VM_ARGUE_GKR_INPUT), the only reader of the lifted
factors left is the zerocheck, and the fused rounds' first pass reads
only their base limb. Under the knob a table's factors stay its resident
base columns: the fused kernels take a row stride (3 for lifted factors,
1 for base columns), a public table is a base copy, and the W*rows*24 B
lift is made only when today's rounds need it - the fused rounds
declined, or the cross-check replays today's beside them. The same
rounds and proof. A table the columns cannot stand in for (a shifted
factor, a public table above the base field, no fused description) is
lifted as before. Off by default; it needs LAMBDA_VM_ARGUE_GKR_INPUT.

- math-cuda: zc_grid01, zc_bus_u, zc_fold2 take a stride;
  sumcheck::FactorView (the fused session's source) and ColumnFactors.
- multilinear: gpu::ColumnFactors and column_factors, gpu_fused's
  FusedSource and FusedInput::columns, batch::prove_resident_lazy (the
  lift made late, counted), the knob and counters; the ARGUE TREE line
  ends with the trees from the columns and the tables with no lift.
- stark: the tall tables' canonical bytes with no lift beside the
  production defaults.
MauroToscano added a commit that referenced this pull request Oct 1, 2026
…TER_LAST=0 the opt-out

FAST job 255 (A B B A, 4 + 4 arms at #1010's head 7d82a32): level 1
-0.40 s, the whole run -0.43 s, the base -0.05 s, the same five program
ids in all eight arms. Without the latch the global child took the card
before the last wide node's artifacts in 3 of 4 arms; with it, in none.

The knob's parse moves into global_after_last_from, so a unit test pins
the default and the opt-out without touching the environment, and a
second one pins the refusal of any other value. The banners now say the
lever is on by default and name =0 as the way off; their leading text is
unchanged, so the gate and readout scripts still match them.
…fault

LAMBDA_VM_ARGUE_GKR_INPUT=0 writes the input layer from the lifted
factors again; LAMBDA_VM_ARGUE_NO_LIFT=0 lifts every table's factors as
before (no lift needs the input from the columns, and its banner says so
when that is off).

FAST job 333 (input from the columns, 4 + 4 arms on #1010's head): base
-0.75 s, the argue -0.97 s, the whole run -0.73 s, the tree region 1.98
-> 1.11 s. FAST job 335 (+ no lift, 4 + 4 arms): base -0.90 s, the whole
run -1.55 s, the argue's reserved peak -1.5 GiB, kept WHIR trees evicted
13 -> 2 a run and the openings' tree rebuilds 0.74 -> 0.04 s. Every arm
of both on the record's program ids. Unit tests pin both defaults.
…'s artifacts (LFM_TREE_GLOBAL_AFTER_LAST, default off)

Level 1 ends when its last wide node proves, and that node's chain is its
prologue, its artifacts, its host prep and its prove. The global child's
prove fits inside that prep at no cost. But the global's card request and
the last node's artifact request arrive within a few hundred milliseconds
of each other, near 5 s into level 1. In 34 arms (jobs 246-292) the global
asked first 11 times, and all 11 read level 1 at 8.0-8.7 s. Each time the
last node's artifacts waited out the global's whole prove, and its prep
then ran with the card idle. No arm under 8.0 s had the global first.

Under LFM_TREE_GLOBAL_AFTER_LAST=1 the global's multi_prove waits behind a
latch that the last wide node opens once its artifacts are built. The
global's host prep keeps its place; only its card request moves.

device_permit gains CardLatch, OpenOnDrop and a thread-local deferral that
applies only to multi_prove holds on an armed permit. The deferral waits
before the queue, never in it, and at most 30 s, after which it queues
anyway and says so. Each deferral prints a CARD DEFER line. The driver
applies the lever only to a wide level 1 whose pool runs the global child
with a worker for every task, so the global can never wait for a node no
worker has taken; otherwise it prints why the lever is inactive. The last
node's opener also opens the latch as its task unwinds.

Only scheduling changes: no proof byte moves.
A level's tasks run one to a thread. The driver now names each task's
proof (the global child, a wide node, a wrap) in a thread-local, and each
CARD HOLD and CARD DEFER trace line ends with "· who=<name>". The suffix
comes after the closing bracket, so existing parsers of these lines are
unaffected.

This lets the global-after-last A/B read the card order directly (whose
multi_prove came before the last node's artifacts) instead of inferring
the holders from their timing. Trace-only: nothing is named when
LFM_CARD_TRACE is off, and the untraced permit path allocates nothing.
…h no global in the pool

The lever's activity is now an explicit mode (Off, Active,
NoGlobalInThePool, TooFewWorkers), and every case prints what it does
when the knob is set. No global child in the level's pool covers a level
of wraps, LFM_TREE_TOP_OVERLAP=0 (the global runs as its own stage) and
the fixture tree. In all of those no latch is made and nothing waits.

Small blocks follow the worker count. Two to fan-in epochs are one wide
node plus the global on at least two workers, so the global waits for
that node's artifacts. A one-epoch block runs on one worker and does not
take the lever.

Unit tests cover these cases next to the deadlock guard's. A device_permit
test covers a thread with no deferral and an opener nobody waits on.
…TER_LAST=0 the opt-out

FAST job 255 (A B B A, 4 + 4 arms at #1010's head 7d82a32): level 1
-0.40 s, the whole run -0.43 s, the base -0.05 s, the same five program
ids in all eight arms. Without the latch the global child took the card
before the last wide node's artifacts in 3 of 4 arms; with it, in none.

The knob's parse moves into global_after_last_from, so a unit test pins
the default and the opt-out without touching the environment, and a
second one pins the refusal of any other value. The banners now say the
lever is on by default and name =0 as the way off; their leading text is
unchanged, so the gate and readout scripts still match them.
@MauroToscano MauroToscano changed the title WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 34.85 s WHIR recursion on GPU (RPX) with ZisK-style proof formats: block 25368371 in 32.62 s Oct 1, 2026
MauroToscano added a commit that referenced this pull request Oct 1, 2026
… lift, latch) into argue/epoch-batch

Conflicts resolved by keeping both sides: gpu.rs keeps #1010's
prove_layer_gruen and the batched argue's LayerSession side by side;
gpu_fused.rs keeps FusedStepper (prove_fused is a loop over it) and takes
#1010's FusedSource, so a stepper reads the lifted factors or the base
columns where they lie; lib.rs declares gkr_gruen and gkr_lockstep.

The batched argue now runs where the per-table argue runs at that head.
Its trees are written from the base columns with no lift, in the same
order the per-table path falls back. Its fused rounds read the base
columns (TableRounds takes the column factors), and a table they cannot
read is lifted as the per-table path lifts it. It prints a phase split
(trees, ladders, zc setup, zc rounds, reduce) under
LAMBDA_VM_BASE_SPLIT=1. The measurement-only knob takes
LAMBDA_VM_ARGUE_BATCHED_MEASURE_CAP to read a bin's peak at another cap.
MauroToscano added a commit that referenced this pull request Oct 1, 2026
WhirBlockPlan::derive takes the statement through the host verifier's own
checks (block_frame), derives the prepared roots from the program, prices
each group in permutations from the closed forms, and partitions the groups
over the leaves (heaviest first onto the least-loaded leaf; leaf 0 carries
the COMMIT-bus target). It never reads a proof.

A leaf replays the statement as constants and the roots block over every
root of the block (the groups' hinted, the prepared ones derived), then for
each of its groups, on the fork S_post || g: the tables' arguments with
their preprocessed legs, the group's opening, the prepared openings. These
are #1010's epoch legs, unchanged. It publishes the block's id, the state,
the output halves and its sum of p/q (minus the target on the carrier).
Every count it hints comes from the shape (hint_table_wires_shaped,
hint_group_chains_shaped); the arena builder refuses a proof whose argument
has other shapes. The plan derives the top program with no proof, and
verify_block_tree checks a top proof against it.

Tests:
- laptop: the group partition, by rule and by refusal;
- run on the laptop with LAMBDA_VM_WHIR_HASH=rpx: the front draws the host's
  z, alpha, beta and a fork's first draw; a leaf refuses a tampered root,
  table argument and prepared opening, and without the prepared openings
  the last tamper executes;
- box: the leaves close the bus; each node binding refuses its tamper over
  real leaves and is load-bearing, including a foreign leaf (state) and a
  carrier that subtracts nothing (bus); the tree proves to the derived top,
  and a tree over another partition is refused there; the real block's
  readout.
MauroToscano added a commit that referenced this pull request Oct 1, 2026
The one-binary whole-block A/B compares #1010's epoch tree and the block
tree on the same code: Gruen GKR rounds, no lift, the global-after-last
latch.

# Conflicts:
#	crypto/multilinear/src/constraint_argument.rs
…as a knob

The WHIR chains (base and W-LFM) ground 20 bits, a constant. The knob makes
the bits a format parameter read at the one production config site
(chain_config_under), so the prover, the host verifier and the in-guest
emitters all take them from the process format, never from a proof. The
query count already subtracts the query grind, so 18 bits raises Q from 112
to 114 at every production height (15..=27 variables); every proven phase
keeps its minimum (binding phase: the unground fold at 27 variables, 130.393;
query phases 130.926 -> 130.907).

Unset is 20: today's configs and proofs, byte for byte. The banner gains
whir_grind_bits=. A chain ground at fewer bits is refused by a stricter
verifier on both the host and the machine, and a proof opened at a Q the
verifier does not expect is refused.

(cherry picked from commit 8c39b95)
…nd 18 grind bits

Prints the emitted chain verifier's real rows per chip, production format,
n 21..=27, under LAMBDA_VM_ZF_WHIR_GRIND_BITS 20 (Q 112) and 18 (Q 114): the
per-chain deltas that size the recursion's table heights under the 18-bit arm.
Asserts nothing; ignored.

(cherry picked from commit 9d51421)
the_w_leg_costs_at_the_sizing_shapes pins the W-leg verifier's closed-form
cost at the process format, and the 18-bit default opens two more queries a
chain (wrap 0: +619 permutations under policy A, +596 under B), so the
pins moved (FAST 665, the one lib-suite failure). A W-LFM plan refuses
artifacts built under another config, so one process costs one setting:
at 18 bits the test checks pins read from the 18-bit form; under
LAMBDA_VM_ZF_WHIR_GRIND_BITS=20 it keeps the 20-bit pins and the design's
0.5 % instrument checks, which were taken at Q 112.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant